SerialPort 类偶尔会在 Dispose 上挂起

本文关键字:Dispose 挂起 偶尔 SerialPort | 更新日期: 2023-09-27 18:31:33

我编写了一个.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;
}

如您所见,它挂在 SerialPort 类的 Dispose 方法上。

做了一些谷歌搜索,我遇到了这个问题:串行端口关闭从此线程挂起应用程序:串行端口在关闭时挂起。 共识似乎是在不同的线程中关闭端口,但这仅适用于表单应用程序吗? 就我而言,我有一个简单的控制台应用程序,所以我认为它不适用(它只是在主线程中的循环中运行)。 我什至不确定这是否真的是这个问题(我的感觉是调制解调器的串行端口驱动程序更有可能出现问题,但我不知道,也许我对调制解调器不公平)。 据我所知,我有三个选择:

  1. 关闭其他线程中的端口
  2. 在关闭端口之前延迟
  3. 让端口永远开放

真的不喜欢任何这些解决方法,但我正在考虑保持端口打开状态,看看会发生什么(我有一种感觉它会泄漏内存或更糟,暴露调制解调器的其他一些问题,但也许我只是悲观,如果是这种情况,我可能会每 24 小时关闭一次, 说,然后再次重新打开它)所以我的问题是...

代码是否存在可能导致此行为的替代问题,或者是否有我上面概述的替代解决方法?

SerialPort 类偶尔会在 Dispose 上挂起

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

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

Dispose() 方法与 Close() 方法执行相同的操作,它向工作线程发出退出信号。 但是,缺陷是它不等待线程退出。 这是一个麻烦的秘诀,在 MSDN 文章中明确记录了 SerialPort.Close() 的"备注"部分:

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

坦率地说,这是"最佳实践"建议的最糟糕的做法,因为它根本没有确切指定您应该等待多长时间。 有充分的理由,没有保证的安全价值。 等待一两秒钟应该是99.9%的好。 当机器负载过重并且工作线程根本没有足够的周期来及时检测关闭条件时,就会发生 0.1% 故障模式。 当然是完全不可调试的。

解决这个问题,只在程序开始时打开一个串行端口,并在退出时关闭它。 除了线程问题之外,这也确保您在另一个程序跳入并从您那里窃取端口时不会随机失去对端口的访问权限。 请注意,实际上不再需要关闭端口,如果您不这样做,Windows 会处理它。

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

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

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

但是,在您的情况下,您已经这样做了..并且挂起是因为端口尚未关闭。 可能 thread.sleep 可能允许端口在您尝试重新打开它之前"稳定"。它可能也是特定于硬件的...这就是为什么没有最佳实践的原因。

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