SerialPort 类偶尔会挂在 Dispose 上

Posted

技术标签:

【中文标题】SerialPort 类偶尔会挂在 Dispose 上【英文标题】:SerialPort class occasionally hangs on Dispose 【发布时间】:2012-04-29 20:19:27 【问题描述】:

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

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 方法上。

我做了一些谷歌搜索,然后我来到了这个问题:Serial Port Close Hangs the application 来自这个线程:serial port hangs whilst closing。共识似乎是在不同的线程中关闭端口,但这仅适用于表单应用程序吗?就我而言,我有一个简单的控制台应用程序,所以我认为它不适用(它只是在主线程的循环中运行)。我什至不确定这实际上是这个问题(我的感觉是调制解调器的串行端口驱动程序更有可能存在问题,但我不知道,也许我对调制解调器不公平)。据我所知,我有三个选择:

    在不同的线程中关闭端口 在关闭端口之前延迟 让端口永远打开

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

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

【问题讨论】:

_command.Execute(..) 里面发生了什么? 您是否尝试过使用建议的解决方法?我知道所有示例都是 Winforms,但the tips article 非常清楚地描述了这个问题。至少值得一试。 它只是发送命令并得到响应(在这种情况下它发送了 AT+CPMS="ME")——虽然我得到了“处理 SerialPort” for" 日志消息,所以我认为它在那里实际做了什么并不太相关?有什么我可以做的事情会导致挂起吗? 好的,所以它只是一个单线程方法,不会产生新线程来执行任何类型的异步通信。 @M.Babcock 还没有尝试过 - 我将添加代码以在不同的线程中关闭,并在 dispose 操作之前延迟,看看会发生什么。 【参考方案1】:

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

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

Dispose() 方法与 Close() 方法做同样的事情,它通知工作线程退出。然而,缺陷在于它不等待线程退出。这是一个麻烦的秘诀,在 MSDN 文章的备注部分中明确记录了 SerialPort.Close() 的类型:

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

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

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

【讨论】:

谢谢,很好的信息 - 我会选择选项 3,然后看看会发生什么!顺便说一句,如果我不关心 ErrorReceived 事件会怎样(这是否意味着这个后台线程根本不会启动)?它挂在 Dispose 上,而不是 Open 所以我认为延迟的事情不会适用吗?【参考方案2】:

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

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

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

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

与 Forms 控件相同: How to remove all event handlers from a control

【讨论】:

以上是关于SerialPort 类偶尔会挂在 Dispose 上的主要内容,如果未能解决你的问题,请参考以下文章

为啥 MySQL 会挂在这个简单的子查询上?

异步事件处理程序有时会挂在 Xamarin.Forms

当客户端连接不稳定时,在 heroku 上运行的 django 中的工作人员会挂在帖子上

为啥打开 mkfifo 管道时我的程序会挂起?

即使光标关闭,Psycopg 请求也会挂起

npm install 挂在 loadIdealTree:loadAllDepsIntoIdealTree: sill install loadIdealTree