问题 SerialPort类偶尔会在Dispose上挂起


我已经编写了一个.net 4.0控制台应用程序,它定期与GSM调制解调器通信,以获取收到的SMS消息列表(它是一个USB调制解调器,但代码通过串口驱动程序连接到它并发送AT命令 - 顺便说一句,它是Sierra无线调制解调器,但我无法更改它,我有最新的驱动程序)。会发生什么事情,经过一段时间(可能是几小时,也许几天),它就会停止工作。这是一个日志片段......

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;
}

正如您所看到的,它挂起在SerialPort类的Dispose方法上。

我做了一些谷歌搜索,我来到这个问题: 串行端口关闭挂起应用程序 从这个线程: 串口在关闭时挂起。双方同意似乎是在一个不同的线程中关闭端口,但这只是一个表单应用程序?在我的情况下,我有一个简单的控制台应用程序,所以我认为它不适用(它只是在主线程中的循环中运行)。我甚至不确定它实际上是这个问题(我的感觉是,调制解调器的串口驱动程序有可能出现问题,但我不知道,也许我对调制解调器不公平)。据我所知,我有三个选择:

  1. 在另一个线程中关闭端口
  2. 在关闭端口之前延迟
  3. 让港口永远开放

我真的不喜欢这些解决方法,但我想把端口打开,看看会发生什么(我觉得它会泄漏内存或更糟,暴露调制解调器的其他问题但也许我只是悲观如果是这种情况,我可能会每24小时关闭一次,比如说再重新打开它,所以我的问题是......

这个代码是否有另一个问题可能导致这个问题,或者是否有其他解决方法来解决上面提到的问题?


4780
2018-04-18 12:10


起源

_command.Execute(..)里面发生了什么? - PeskyGnat
您是否尝试使用建议的解决方法?我知道所有的例子都是Winforms,但是 提示文章 很清楚地描述了这个问题。值得至少尝试一下。 - M.Babcock
它只是发送命令并得到一个响应(在这种情况下,它发送AT + CPMS =“我”) - 虽然因为我得到了“Disposing the SerialPort for”日志消息所以我认为它很好它在那里实际上做的并不太相关?我可以做些什么会导致挂起? - kmp
好吧,所以它只是一个单线程方法,它不会产生一个新线程来进行任何类型的异步通信。 - PeskyGnat
@ M.Babcock尚未尝试过它们 - 我将添加代码以在另一个线程中关闭并在处理操作之前加入延迟并查看会发生什么。 - kmp


答案:


SerialPort有点容易死锁。到目前为止,最常见的原因是您找到的原因,它是通过在DataReceived事件处理程序中使用Invoke()来触发的。显然不是你的情况。

这些死锁与SerialPort幕后启动的工作线程有关。该线程有助于检测端口上的异步事件,底层的本机winapi是WaitCommEvent()。该工作程序使DataReceived,PinChanged和ErrorReceived事件起作用。请注意你  使用ErrorReceived。

Dispose()方法与Close()方法的作用相同,它指示工作线程退出。但缺点是它 不等 让线程退出。这是一个麻烦的方法,这是在备注部分的SerialPort.Close()的MSDN文章中明确记录的那种:

任何应用程序的最佳实践是在尝试调用Open方法之前等待一段时间后再调用Close方法,因为端口可能不会立即关闭。

坦率地说,这是“最佳实践”建议最糟糕的做法,因为它根本没有说明你应该等多久。有充分理由,没有保证的安全价值。等待一两秒应该是99.9%的好。当机器负载很重并且工作线程没有足够的周期来及时检测到关闭状态时,会发生0.1%故障模式。当然是完全不可取的。

解决这个问题,只能在程序开始时打开一个串口,并在退出时关闭它。除了线程故障之外,这还可以确保当另一个程序跳入并将端口远离您时,您不会随机丢失对该端口的访问权限。请注意,关闭端口实际上不再需要,如果不这样做,Windows将会处理它。


12
2018-04-18 13:21



谢谢,好消息 - 我会选择3然后看看会发生什么!顺便说一句,如果我只是不打扰ErrorReceived事件(这意味着这个后台线程根本不会启动)怎么办?并且它挂在Dispose上,而不是Open,所以我认为延迟的东西不会适用它吗? - kmp


如果您正在使用DataRecieved事件或串行端口对象中的任何其他事件,则应在处置串行端口之前从中删除事件处理程序。

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

发生挂起是因为您在已处置的对象上触发事件...这显然是一个错误。

但是,在您的情况下,您已经完成了...并且因为端口尚未关闭而发生挂起。可能一个thread.sleep可能允许端口在尝试重新打开之前“解决”。它可能也是特定的硬件......这就是没有最佳实践的原因。

与Forms控件相同: 如何从控件中删除所有事件处理程序


2
2017-07-23 18:01