簡體   English   中英

在.Net 2.0中關閉SerialPort時出現ObjectDisposedException

[英]ObjectDisposedException when closing SerialPort in .Net 2.0

我有一個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: 

我找到了有類似問題的人的帖子,並嘗試了解決方法[這里] [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上的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。 在處理時,檢查此屬性並跳過關閉內部串行流,從而無限期地保持原始句柄打開(直到終結器運行導致下一個問題)。

    這個解決方案是手動關閉內部串行流。 我們可以在異常發生之前通過SerialPort.BaseStream獲取它的引用,或者通過反射並獲取“internalSerialStream”字段。

  3. 刪除USB串行設備時,關閉內部串行流會引發異常並關閉內部句柄,而不等待其eventLoopRunner線程完成,從而在流的終結器運行時從后台事件循環運行程序線程中導致無法捕獲的ObjectDisposedException(其中奇怪地避免拋出異常,但仍然無法等待eventLoopRunner)。

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

    解決方案是手動要求事件循環運行器停止(通過反射)並在關閉內部串行流之前等待它完成。

  4. 由於Dispose拋出異常,因此不會抑制終結器。 這很容易解決:

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

這是一個包裝串口並修復所有這些問題的類: http //pastebin.com/KmKEVzR8

使用此解決方法類,不需要恢復到.NET 1.1未處理的異常行為,並且它具有出色的穩定性。

這是我的第一個貢獻,所以如果我做得不對,請原諒。 我希望它對某人有幫助。

是的,SerialPort類中存在一個缺陷,使得這種崩潰成為可能。 調用Open()時,SerialPort啟動一個線程。 該線程監視端口上的事件,例如,您獲取DataReceived事件的方式。 當你調用BaseStream.Close()或Close()或Dispose()方法(它們都做同樣的事情)時,SerialPort只要求線程退出但不等待它退出。

這會導致各種問題。 一個記錄的,你不應該在關閉后立即打開()一個端口。 但這里的不幸事件是你的程序退出或者在Close()調用之后立即收集垃圾。 它運行終結器並嘗試關閉句柄。 它仍處於打開狀態,因為工作線程仍在使用它。 現在可以進行螺紋加工,這不是正確的互鎖。 kaboom發生在工作人員設法關閉句柄並在終結器線程嘗試執行相同操作之前退出時。 異常是無法捕獲的,因為它發生在終結器線程中,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]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM