在.Net 2.0中关闭SerialPort时出现ObjectDisposedException

我有一个C#windows窗体应用程序,它通过COM端口与USB加密狗通信。 我正在使用.Net 2.0中的SerialPort类进行通信,并且串行端口对象在应用程序的生命周期内是打开的。 应用程序向设备发送命令,还可以从设备接收未经请求的数据。

表单关闭时出现问题 – 在尝试关闭COM端口时,我(随机地,不幸地)得到了ObjectDisposedException。 这是Windows堆栈跟踪:

System.ObjectDisposedException was unhandled Message=Safe handle has been closed Source=System ObjectName="" StackTrace: at Microsoft.Win32.UnsafeNativeMethods.SetCommMask(SafeFileHandle hFile, Int32 dwEvtMask) at System.IO.Ports.SerialStream.Dispose(Boolean disposing) at System.IO.Ports.SerialStream.Finalize() InnerException: 

我找到了有类似问题的人的post,并尝试了解决方法[这里] [1]

[1]:http: //zachsaw.blogspot.com/2010/07/net-serialport-woes.html虽然这是针对IOException并且没有阻止问题。

我的Close()代码如下:

  public void Close() { try { Console.WriteLine("******ComPort.Close - baseStream.Close*******"); baseStream.Close(); } catch (Exception ex) { Console.WriteLine("******ComPort.Close baseStream.Close raised exception: " + ex + "*******"); } try { _onDataReceived = null; Console.WriteLine("******ComPort.Close - _serialPort.Close*******"); _serialPort.Close(); } catch (Exception ex) { Console.WriteLine("******ComPort.Close - _serialPort.Close raised exception: " + ex + "*******"); } } 

我的日志记录显示执行从未超出尝试关闭SerialPort的BaseStream(这是在第一个try块中),所以我尝试删除此行但是exception仍然会定期抛出 – 第二个try块中的日志记录然后出现exception发生了。 catch块都没有捕获exception。

有任何想法吗?

更新 – 添加完整课程:

  namespace My.Utilities { public interface ISerialPortObserver { void SerialPortWriteException(); } internal class ComPort : ISerialPort { private readonly ISerialPortObserver _observer; readonly SerialPort _serialPort; private DataReceivedDelegate _onDataReceived; public event DataReceivedDelegate OnDataReceived { add { lock (_dataReceivedLocker) { _onDataReceived += value; } } remove { lock (_dataReceivedLocker) { _onDataReceived -= value; } } } private readonly object _dataReceivedLocker = new object(); private readonly object _locker = new object(); internal ComPort() { _serialPort = new SerialPort { ReadTimeout = 10, WriteTimeout = 100, DtrEnable = true }; _serialPort.DataReceived += DataReceived; } internal ComPort(ISerialPortObserver observer) : this() { _observer = observer; } private void DataReceived(object sender, SerialDataReceivedEventArgs e) { DataReceivedDelegate temp = null; lock (_locker) { lock (_dataReceivedLocker) { temp = _onDataReceived; } string dataReceived = string.Empty; var sp = (SerialPort) sender; try { dataReceived = sp.ReadExisting(); } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.DataReceived raised exception: " + ex); } if (null != temp && string.Empty != dataReceived) { try { temp(dataReceived, TickProvider.GetTickCount()); } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.DataReceived raised exception calling handler: " + ex); } } } } public string Port { set { try { _serialPort.PortName = value; } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.Port raised exception: " + ex); } } } private System.IO.Stream comPortStream = null; public bool Open() { SetupSerialPortWithWorkaround(); try { _serialPort.Open(); comPortStream = _serialPort.BaseStream; return true; } catch (Exception ex) { Logger.Log(TraceLevel.Warning, "ComPort.Open raised exception: " + ex); return false; } } public bool IsOpen { get { SetupSerialPortWithWorkaround(); try { return _serialPort.IsOpen; } catch(Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.IsOpen raised exception: " + ex); } return false; } } internal virtual void SetupSerialPortWithWorkaround() { try { //http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html // This class is meant to fix the problem in .Net that is causing the ObjectDisposedException. SerialPortFixer.Execute(_serialPort.PortName); } catch (Exception e) { Logger.Log(TraceLevel.Info, "Work around for .Net SerialPort object disposed exception failed with : " + e + " Will still attempt open port as normal"); } } public void Close() { try { comPortStream.Close(); } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPortStream.Close raised exception: " + ex); } try { _onDataReceived = null; _serialPort.Close(); } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.Close raised exception: " + ex); } } public void WriteData(string aData, DataReceivedDelegate handler) { try { OnDataReceived += handler; _serialPort.Write(aData + "\r\n"); } catch (Exception ex) { Logger.Log(TraceLevel.Error, "ComPort.WriteData raised exception: " + ex); if (null != _observer) { _observer.SerialPortWriteException(); } } } } } 

注意:目前的调查结果仅在Windows 7上的32位.NET Framework 4.0上进行了测试,如果适用于其他版本,请随时发表评论。

编辑: TL; DR:这是解决方法的关键。 请参阅下面的说明。 打开SerialPort时不要忘记使用SerialPortFixer 。 ILog来自log4net。

 static readonly ILog s_Log = LogManager.GetType("SerialWorkaroundLogger"); static void SafeDisconnect(SerialPort port, Stream internalSerialStream) { GC.SuppressFinalize(port); GC.SuppressFinalize(internalSerialStream); ShutdownEventLoopHandler(internalSerialStream); try { s_Log.DebugFormat("Disposing internal serial stream"); internalSerialStream.Close(); } catch (Exception ex) { s_Log.DebugFormat( "Exception in serial stream shutdown of port {0}: {1}", port.PortName, ex); } try { s_Log.DebugFormat("Disposing serial port"); port.Close(); } catch (Exception ex) { s_Log.DebugFormat("Exception in port {0} shutdown: {1}", port.PortName, ex); } } static void ShutdownEventLoopHandler(Stream internalSerialStream) { try { s_Log.DebugFormat("Working around .NET SerialPort class Dispose bug"); FieldInfo eventRunnerField = internalSerialStream.GetType() .GetField("eventRunner", BindingFlags.NonPublic | BindingFlags.Instance); if (eventRunnerField == null) { s_Log.WarnFormat( "Unable to find EventLoopRunner field. " + "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file."); } else { object eventRunner = eventRunnerField.GetValue(internalSerialStream); Type eventRunnerType = eventRunner.GetType(); FieldInfo endEventLoopFieldInfo = eventRunnerType.GetField( "endEventLoop", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo eventLoopEndedSignalFieldInfo = eventRunnerType.GetField( "eventLoopEndedSignal", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo waitCommEventWaitHandleFieldInfo = eventRunnerType.GetField( "waitCommEventWaitHandle", BindingFlags.Instance | BindingFlags.NonPublic); if (endEventLoopFieldInfo == null || eventLoopEndedSignalFieldInfo == null || waitCommEventWaitHandleFieldInfo == null) { s_Log.WarnFormat( "Unable to find the EventLoopRunner internal wait handle or loop signal fields. " + "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file."); } else { s_Log.DebugFormat( "Waiting for the SerialPort internal EventLoopRunner thread to finish..."); var eventLoopEndedWaitHandle = (WaitHandle)eventLoopEndedSignalFieldInfo.GetValue(eventRunner); var waitCommEventWaitHandle = (ManualResetEvent)waitCommEventWaitHandleFieldInfo.GetValue(eventRunner); endEventLoopFieldInfo.SetValue(eventRunner, true); // Sometimes the event loop handler resets the wait handle // before exiting the loop and hangs (in case of USB disconnect) // In case it takes too long, brute-force it out of its wait by // setting the handle again. do { waitCommEventWaitHandle.Set(); } while (!eventLoopEndedWaitHandle.WaitOne(2000)); s_Log.DebugFormat("Wait completed. Now it is safe to continue disposal."); } } } catch (Exception ex) { s_Log.ErrorFormat( "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file: {0}", ex); } } 

在最近的一个项目中,我已经在这里挣了几天。

使用.NET SerialPort类有许多不同的错误(我到目前为止已经看到)导致网络上的所有问题。

  1. 缺少的DCB结构标志在这里:http: //zachsaw.blogspot.com/2010/07/net-serialport-woes.html这个由SerialPortFixer类修复,为作者提供信用。

  2. 删除USB串行设备时,关闭SerialPortStream时,会要求eventLoopRunner停止,并且SerialPort.IsOpen返回false。 处理完此属性后,将跳过关闭内部串行流,从而无限期地保持原始句柄打开(直到终结器运行导致下一个问题)。

    这个解决方案是手动关闭内部串行流。 我们可以在exception发生之前通过SerialPort.BaseStream获取它的引用,或者通过reflection并获取“internalSerialStream”字段。

  3. 删除USB串行设备时,关闭内部串行流会引发exception并关闭内部句柄而不等待其eventLoopRunner线程完成,从而在流的终结器运行时从后台事件循环运行程序线程导致无法捕获的ObjectDisposedException(其中奇怪地避免抛出exception,但仍然无法等待eventLoopRunner)。

    这里的症状: https : //connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port

    解决方案是手动要求事件循环运行器停止(通过reflection)并在关闭内部串行流之前等待它完成。

  4. 由于Dispose抛出exception,因此不会抑制终结器。 这很容易解决:

    GC.SuppressFinalize(端口); GC.SuppressFinalize(port.BaseStream);

这是一个包装串口并修复所有这些问题的类: http //pastebin.com/KmKEVzR8

使用此解决方法类,不需要恢复到.NET 1.1未处理的exception行为,并且它具有出色的稳定性。

这是我的第一个贡献,所以如果我做得不对,请原谅。 我希望它对某人有帮助。

是的,SerialPort类中存在一个缺陷,可以使这种崩溃成为可能。 调用Open()时,SerialPort启动一个线程。 该线程监视端口上的事件,例如,您获取DataReceived事件的方式。 当你调用BaseStream.Close()或Close()或Dispose()方法(它们都做同样的事情)时,SerialPort只要求线程退出但不等待它退出。

这会导致各种问题。 一个记录的,你不应该在关闭它后立即打开()一个端口。 但这里发生的事故是你的程序退出或者在Close()调用之后立即收集垃圾。 它运行终结器,它也试图关闭句柄。 它仍处于打开状态,因为工作线程仍在使用它。 现在可以进行螺纹加工,这不是正确的互锁。 kaboom发生在工作人员设法关闭句柄并在终结器线程尝试执行相同操作之前退出时。 exception是无法捕获的,因为它发生在终结器线程中,CLR中止程序。

从2.0开始的每个.NET版本在类中都有很小的变化来解决SerialPort问题。 到目前为止,如果你还在.NET 2.0上,最好的办法就是不要实际调用Close()。 无论如何,它会自动发生,终结器会处理它。 即使由于某种原因(硬崩溃或程序中止)没有发生这种情况,Windows也会确保端口关闭。

我知道这是一个相当陈旧的问题。 我最近遇到了这个问题,在寻找解决方案之后,根据发行说明,看起来这个问题最终是通过.NET Framework 4.7修复的。 https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md

修复了SerialPort中的一个问题,即在执行期间拔出设备可能会导致SerialStream类中的内存泄漏。 [288363]