关闭.Net 2.0中的SerialPort时出现ObjectDisposedException
本文关键字:ObjectDisposedException SerialPort 中的 Net 关闭 | 更新日期: 2023-09-27 18:20:42
我有一个C#windows窗体应用程序,它通过COM端口与USB加密狗通信。我使用.Net 2.0中的SerialPort类进行通信,串行端口对象在应用程序的整个生命周期内都是打开的。应用程序向设备发送命令,还可以从设备接收未经请求的数据。
我的问题发生在表单关闭时-不幸的是,我在试图关闭COM端口时(随机地)得到了一个ObjectDisposedException
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:
我发现了有类似问题的人发来的帖子,并尝试了变通方法[此处][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
块中),所以我尝试删除这一行,但异常仍然会定期抛出——第二个try
块中的日志记录出现,然后异常发生。两个catch块都没有捕获异常。
有什么想法吗?
更新-添加完整类:
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上的.NET Framework 4.0 32位上进行了测试,如果它在其他版本上有效,请随时发表评论
编辑:TL;博士:以下是解决方法的关键。请参阅下面的说明。打开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类有很多不同的错误(到目前为止我已经看到了),这些错误导致了网络上所有令人头疼的问题。
此处缺少DCB结构标志:http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html这一个是由SerialPortFixer类修复的,该类的学分归作者所有。
移除USB串行设备后,关闭SerialPortStream时,会要求事件LoopRunner停止,SerialPort.IsOpen返回false。在处理时,会检查此属性,并跳过关闭内部串行流,从而无限期地保持原始句柄的打开状态(直到终结器运行,从而导致下一个问题)。
这个问题的解决方案是手动关闭内部串行流。我们可以在异常发生之前通过SerialPort.BaseStream或通过反射获得它的引用以及获取"internalSerialStream"字段。
当USB串行设备被移除时,关闭内部串行流会引发异常并关闭内部句柄,而无需等待其eventLoopRunner线程完成,稍后当流的终结器运行时,从后台事件循环运行线程中引发不可捕获的ObjectDisposedException(奇怪的是,这避免了引发异常,但仍然无法等待eventLoopRunner)。
这里的症状:https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port
解决方案是手动要求事件循环运行程序停止(通过反射)并等待它完成,然后关闭内部串行流。
由于Dispose抛出异常,因此终结器不会被抑制。这很容易解决:
GC.SuppressFinalize(端口);GC.SuppressFinalize(port.BaseStream);
下面是一个封装串行端口并修复所有这些问题的类:http://pastebin.com/KmKEVzR8
有了这个变通方法类,恢复到.NET 1.1未处理的异常行为是不必要的,而且它的工作稳定性非常好。
这是我的第一次贡献,如果我做得不对,请原谅我。我希望它能帮助到别人。
是的,SerialPort类中有一个缺陷,导致了这种崩溃。SerialPort在调用Open()时启动线程。该线程监视端口上的事件,例如,这就是获取DataReceived事件的方式。当您调用BaseStream.Close()、Close()或Dispose()方法(它们都做同样的事情)时,SerialPort仅要求线程退出,但不等待它退出。
这会导致各种各样的问题。一个有文档记载的例子是,你不应该在关闭端口后立即打开()。但这里的错误是,当你的程序在Close()调用后立即退出或垃圾收集时。它运行终结器,并尝试关闭句柄。它仍然是打开的,因为工作线程仍在使用它。线程竞争现在是可能的,这没有正确地互锁。当工作线程在终结器线程尝试关闭句柄并退出时,就会发生kaboom。异常是不可捕获的,因为它发生在终结器线程中,CLR会中止程序。
自2.0以来的每一个.NET版本都对类进行了小的更改,以解决串行端口问题。到目前为止,如果您仍在.NET 2.0上,最好的做法是而不是实际调用Close()。无论如何,它都会自动发生,终结器会处理它。即使由于某种原因(硬崩溃或程序中止)没有发生,Windows也会确保端口关闭。
我知道这是一个非常古老但当前的问题。我最近遇到了这个问题,根据发布说明,在寻找解决方案后,这个问题似乎终于用.NET Framework 4.7解决了。https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md
修复了SerialPort中的一个问题,在执行过程中拔下设备可能会导致SerialStream类中的内存泄漏。[288363]