Класс SerialPort иногда зависает на Dispose
Я написал консольное приложение.net 4.0, которое периодически обращается к GSM-модему, чтобы получить список полученных SMS-сообщений (это USB-модем, но код подключается к нему через драйвер последовательного порта и отправляет AT-команды - случайно это модем Sierra Wireless, но я не могу его изменить, и у меня установлена последняя версия драйвера). Происходит то, что через некоторое время (может быть, часы, может быть, дни) он просто перестает работать. Вот фрагмент журнала...
2012-04-17 23:07:31 DEBUG Modem Check (108) - Executing AT command 'AT+CPMS="ME"'...
2012-04-17 23:07:31 DEBUG Modem Check (108) - Finished executing 'AT+CPMS="ME"'
2012-04-17 23:07:31 DEBUG Modem Check (108) - Detaching event handlers for 'COM13'
2012-04-17 23:07:31 DEBUG Modem Check (108) - Disposing the SerialPort for 'COM13'
Это конец журнала - ничего более, хотя я ожидаю увидеть хотя бы еще одно утверждение, вот соответствующий код:
internal T Execute()
{
var modemPort = new SerialPort();
T ret;
try
{
modemPort.ErrorReceived += ModemPortErrorReceived;
modemPort.PortName = _descriptor.PortName;
modemPort.Handshake = Handshake.None;
modemPort.DataBits = 8;
modemPort.StopBits = StopBits.One;
modemPort.Parity = Parity.None;
modemPort.ReadTimeout = ReadTimeout;
modemPort.WriteTimeout = WriteTimeout;
modemPort.NewLine = "\r\n";
modemPort.BaudRate = _descriptor.Baud;
if (!modemPort.IsOpen)
{
modemPort.Open();
}
ret = _command.Execute(modemPort, _logger);
_logger.Debug("Detaching event handlers for '{0}'",
_descriptor.PortName);
modemPort.ErrorReceived -= ModemPortErrorReceived;
_logger.Debug("Disposing the SerialPort for '{0}'",
_descriptor.PortName);
}
catch (IOException ex)
{
_logger.Error(ex.Message);
throw new CommandException(
string.Format(CultureInfo.CurrentCulture,
ModemWrapperStrings.COMMAND_ERROR,
ex.Message),
ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.Error(ex.Message);
throw new CommandException(
string.Format(CultureInfo.CurrentCulture,
ModemWrapperStrings.COMMAND_ERROR,
ex.Message),
ex);
}
finally
{
modemPort.Dispose();
_logger.Debug("Modem on port '{0}' disposed",
_descriptor.PortName);
}
return ret;
}
Как вы можете видеть, он зависает от метода Dispose класса SerialPort.
Я немного погуглил и пришел к этой проблеме: Закрытие последовательного порта Зависает приложение из этого потока: зависает последовательный порт при закрытии. Кажется, что консенсусом является закрытие порта в другом потоке, но это только для приложения форм? В моем случае у меня есть простое консольное приложение, поэтому я не думаю, что оно применимо (оно просто запускается в цикле в главном потоке). Я даже не уверен, что это на самом деле эта проблема (у меня такое ощущение, что более вероятно, что есть проблема с драйвером последовательного порта от модема, но я не знаю, и, возможно, я несправедлив по отношению к модему). Насколько я вижу, у меня есть три варианта:
- Закройте порт в другом потоке
- Задержка перед закрытием порта
- Оставьте порт открытым навсегда
Мне не очень нравятся какие-либо из этих обходных путей, но я думаю оставить порт открытым и просто посмотреть, что произойдет (у меня такое ощущение, что утечка памяти или что-то еще хуже, выявить некоторые другие проблемы с модемом, но, возможно, я просто пессимистичен и если это так, я мог бы, вероятно, сойти с рук, закрыв каждые 24 часа, скажем, и снова открыв его снова), поэтому мой вопрос...
Есть ли альтернативная проблема с этим кодом, которая может вызывать такое поведение, или есть альтернативный обходной путь к тому, что я обрисовал в общих чертах выше?
2 ответа
SerialPort несколько склонен к тупику. Безусловно, наиболее распространенной причиной является та, которую вы нашли, она вызывается с помощью Invoke() в обработчике событий DataReceived. Ясно, что не ваш случай здесь.
Эти взаимоблокировки связаны с рабочим потоком, который SerialPort запускает за кулисами. Этот поток помогает обнаруживать асинхронные события на порте, основной собственный winapi - WaitCommEvent(). Этот работник заставляет работать события DataReceived, PinChanged и ErrorReceived. Обратите внимание, как вы используете ErrorReceived.
Метод Dispose() делает то же самое, что и метод Close(), он сигнализирует рабочему потоку о выходе. Недостаток, однако, заключается в том, что он не ожидает завершения потока. Это рецепт для неприятностей, вид, который явно задокументирован в статье MSDN для SerialPort.Close() в разделе "Примечания":
Для любого приложения рекомендуется подождать некоторое время после вызова метода Close, прежде чем пытаться вызвать метод Open, поскольку порт может не закрыться мгновенно.
Честно говоря, это наихудшая из возможных рекомендаций для "наилучшей практики", поскольку она совсем не определяет, как долго вы должны ждать. По уважительной причине не существует гарантированного безопасного значения. Ожидание секунды или двух должно быть хорошо на 99,9%. Режим отказа 0,1% возникает, когда машина сильно загружена, а рабочий поток просто не получает достаточно циклов, чтобы вовремя обнаружить состояние закрытия. Совершенно неразборчиво, конечно.
Решите эту проблему, только когда-либо открывайте последовательный порт в начале вашей программы и закрывайте его при выходе. За исключением проблем с многопоточностью, это также гарантирует, что вы не потеряете случайным образом доступ к порту, когда другая программа подключится и украдет порт у вас. И обратите внимание, что закрытие порта больше не нужно, Windows позаботится об этом, если вы этого не сделаете.
Если вы используете событие DataRectained или любые другие события из вашего объекта последовательного порта, вы должны удалить из него обработчики событий перед удалением последовательного порта.
mySerial.DataReceived -= DataReceivedHandler;
mySerial.Dispose();
Зависание происходит из-за того, что у вас есть событие, запускаемое на удаленном объекте... что, очевидно, является ошибкой.
Тем не менее, в вашем случае вы сделали это... и зависание происходит, потому что порт не закрыт. возможно, thread.sleep может позволить порту "обосноваться", прежде чем вы попытаетесь снова открыть его. Это, вероятно, также зависит от аппаратного обеспечения... вот почему нет лучшей практики.
То же, что и для элемента управления Forms: как удалить все обработчики событий из элемента управления