Класс 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.

Я сделал некоторый Googling, и я пришел к этой проблеме: Serial Port Close Завершает приложение из этого потока: последовательный порт висит во время закрытия. Кажется, что консорциум закрывает порт в другом потоке, но это просто для приложения форм? В моем случае у меня есть простое консольное приложение, поэтому я не думаю, что он применяется (он просто работает в цикле в основном потоке). Я даже не уверен, что на самом деле это проблема (я чувствую, что вероятность того, что с драйвером последовательного порта связана с модемом, но я не знаю и, возможно, я несправедлив к модему). Насколько я понимаю, у меня есть три варианта:

  • Закройте порт в другом потоке.
  • Перед закрытием порта поставьте задержку
  • Оставьте порт открытым навсегда

Мне не нравятся какие-либо из этих обходных решений, но я собираюсь оставить порт открытым и просто посмотреть, что происходит (у меня такое ощущение, что он просочится в память или что-то еще хуже, выложите некоторые другие проблемы с модемом, но, возможно, я я просто пессимистичен, и если это так, я мог бы уйти, закрыв его каждые 24 часа, скажем, и снова открыв его), поэтому мой вопрос...

Есть ли альтернатива с этим кодом, который может вызвать этот bevahior или есть альтернативный способ обхода описанного выше?

Ответ 1

SerialPort несколько подвержен тупиковой ситуации. Безусловно, наиболее распространенной причиной является то, что вы нашли, оно запускается с помощью Invoke() в обработчике событий DataReceived. Очевидно, что не ваш случай здесь.

Эти взаимоблокировки связаны с рабочим потоком, который SerialPort начинает за завесой. Этот поток помогает обнаруживать асинхронные события на порту, основным родным winapi является WaitCommEvent(). Этот рабочий делает операции DataReceived, PinChanged и ErrorReceived. Обратите внимание, как вы используете ErrorReceived.

Метод Dispose() выполняет то же самое, что и метод Close(), он сигнализирует о выходе рабочего потока. Недостатком является то, что он не дожидается выхода потока. Это рецепт проблемы, вид, который явно задокументирован в статье MSDN для SerialPort.Close() в разделе "Примечания":

Лучшей практикой для любого приложения является ожидание некоторого времени после вызова метода Close, прежде чем пытаться вызвать метод Open, поскольку он не может быть немедленно закрыт.

То, что, честно говоря, самая худшая практика для рекомендации "лучшей практики", поскольку она совсем не указывает, как долго вы должны ждать. По уважительной причине нет гарантированного безопасного значения. Ожидание второго или двух должно быть 99,9%. Режим отказа 0,1% возникает, когда машина сильно загружена, а рабочий поток просто не получает достаточного количества циклов для обнаружения условия закрытия во времени. Разумеется, безусловно, undebuggable.

Поверните эту проблему, только когда-либо открывайте последовательный порт в начале вашей программы и закрывайте его при выходе. Это связано с тем, что вы не случайно теряете доступ к порту, когда другая программа перескакивает и крадет порт от вас. И обратите внимание, что закрытие порта больше не требуется, Windows позаботится об этом, если вы этого не сделаете.

Ответ 2

Если вы используете событие DataRecieved или любые другие события из вашего объекта последовательного порта, вы должны удалить из него обработчик событий, прежде чем удалять последовательный порт.

mySerial.DataReceived -= DataReceivedHandler;
mySerial.Dispose();

Повреждение происходит потому, что у вас есть событие, срабатывающее на удаленном объекте... что, очевидно, является ошибкой.

Однако в вашем случае вы это сделали.. и зависание происходит, потому что порт не закрыт. возможно, thread.sleep может позволить порту "уладить", прежде чем пытаться его снова открыть. Вероятно, это и аппаратное обеспечение... вот почему нет лучшей практики.

То же, что и для элемента управления Forms: Как удалить все обработчики событий из элемента управления