C# 串口關閉時主界面卡死原因分析


問題描述

前幾天用SerialPort類寫一個串口的測試程序,關閉串口的時候會讓界面卡死。
參考博客windows程序界面卡死的原因,得出界面卡死原因:主線程和其他的線程由於資源或者鎖爭奪,出現了死鎖。

參考知乎文章WinForm界面假死,如何判斷其卡在代碼中的哪一步?,通過點擊調試暫停,查看ui線程函數棧,直接定位阻塞代碼的行數,確定問題出現在SerialPort類的Close()方法。

參考文章C# 串口操作系列(2) -- 入門篇,為什么我的串口程序在關閉串口時候會死鎖 ?文章的解決方法和網上的大部分解決方法類似:定義2個bool類型的標記Listening和Closing,關閉串口和接受數據前先判斷一下。我個人並不太接受這種方法,感覺還有更好的方式,而且文章講述的也並不太清楚。

查找原因

基於刨根問底的原則,我繼續查找問題發生的原因。
先看看導致界面卡死的代碼:

void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)   
{   
    //獲取串口讀取的字節數
    int n = comm.BytesToRead;    
    //讀取緩沖數據  
    comm.Read(buf, 0, n);       
    //因為要訪問ui資源,所以需要使用invoke方式同步ui。   
    this.Invoke(new Action(() =>{...界面更新,略})); 
}   
  
private void buttonOpenClose_Click(object sender, EventArgs e)   
{   
    //根據當前串口對象,來判斷操作   
    if (comm.IsOpen)   
    {   
        //打開時點擊,則關閉串口   
        comm.Close();//界面卡死的原因
    }   
    else  
    {...}  
}

問題就出現在上面的代碼中,原理目前還不明確,我只能參考.NET源碼來查找問題。

SerialPort類Open()方法

SerialPort類Close()方法的源碼如下:

		public void Open()
        {
           //省略部分代碼...
            internalSerialStream = new SerialStream(portName, baudRate, parity, dataBits, stopBits, readTimeout,
                writeTimeout, handshake, dtrEnable, rtsEnable, discardNull, parityReplace);
 
            internalSerialStream.SetBufferSizes(readBufferSize, writeBufferSize); 
            internalSerialStream.ErrorReceived += new SerialErrorReceivedEventHandler(CatchErrorEvents);
            internalSerialStream.PinChanged += new SerialPinChangedEventHandler(CatchPinChangedEvents);
            internalSerialStream.DataReceived += new SerialDataReceivedEventHandler(CatchReceivedEvents);
        } 

每次執行SerialPort類Open()方法都會出現實例化一個SerialStream類型的對象,並將CatchReceivedEvents事件處理程序綁定到SerialStream實例的DataReceived事件。

SerialStream類CatchReceivedEvents方法的源碼如下:

        private void CatchReceivedEvents(object src, SerialDataReceivedEventArgs e)
        {
            SerialDataReceivedEventHandler eventHandler = DataReceived;
            SerialStream stream = internalSerialStream;
 
            if ((eventHandler != null) && (stream != null)){
                lock (stream) {
                    bool raiseEvent = false;
                    try {
                        raiseEvent = stream.IsOpen && (SerialData.Eof == e.EventType || BytesToRead >= receivedBytesThreshold);    
                    }
                    catch {
                        // Ignore and continue. SerialPort might have been closed already! 
                    }
                    finally {
                        if (raiseEvent)
                            eventHandler(this, e);  // here, do your reading, etc. 
                    }
                }
            }
        }
 

可以看到SerialStream類CatchReceivedEvents方法觸發自身的DataReceived事件,這個DataReceived事件就是我們處理串口接收數據的用到的事件。

DataReceived事件處理程序是在lock (stream) {...}塊中執行的,ErrorReceived 、PinChanged 也類似。

SerialPort類Close()方法

SerialPort類Close()方法的源碼如下:

		// Calls internal Serial Stream's Close() method on the internal Serial Stream.
        public void Close()
        {
            Dispose();
        }
        
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected override void Dispose( bool disposing )
        {
            if( disposing ) {
                if (IsOpen) {
                    internalSerialStream.Flush();
                    internalSerialStream.Close();
                    internalSerialStream = null;
                }
            }
            base.Dispose( disposing );
        }        

可以看到,執行Close()方法最終會調用Dispose( bool disposing )方法。
微軟SerialPort類對父類的Dispose( bool disposing )方法進行了重寫,在執行base.Dispose( disposing )前會執行internalSerialStream.Close()方法,也就是說SerialPort實例執行Close()方法時會先關閉SerialPort實例內部的SerialStream實例,再執行父類的Close()操作

base.Dispose( disposing )方法不作為重點,我們再看internalSerialStream.Close()方法。

SerialStream類源碼沒有找到Close()方法,說明沒有重寫父類的Close方法,直接看父類的Close()方法,源碼如下:

		public virtual void Close()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }        

SerialStream父類的Close方法調用了Dispose(true),不過SerialStream類重寫了父類的Dispose(bool disposing)方法,源碼如下:

		protected override void Dispose(bool disposing)
        {
            if (_handle != null && !_handle.IsInvalid) {
                try {
                //省略一部分代碼
                }
                finally {
                    // If we are disposing synchronize closing with raising SerialPort events
                    if (disposing) {
                        lock (this) {
                            _handle.Close();
                            _handle = null;
                        }
                    }
                    else {
                        _handle.Close();
                        _handle = null;
                    }
                    base.Dispose(disposing);
                }
            }
        }

SerialStream父類的Close方法調用了Dispose(true),上面的代碼一定會執行到lock (this) 語句,也就是說SerialStream實例執行Close()方法時會lock自身

死鎖原因

把我們前面源碼分析的結果總結一下:

  • DataReceived事件處理程序是在lock (stream) {...}塊中執行的
  • SerialPort實例執行Close()方法時會先關閉SerialPort實例內部的SerialStream實例
  • SerialStream實例執行Close()方法時會lock實例自身

當輔助線程調用DataReceived事件處理程序處理串口數據但還未更新界面時,點擊界面“關閉”按鈕調用SerialPort實例的Close()方法,UI線程會在lock(stream)處一直等待輔助線程釋放stream的線程鎖。
當輔助線程處理完數據准備更新界面時問題來了,DataReceived事件處理程序中的this.Invoke()一直會等待UI線程來執行委托,但此時UI線程還停在SerialPort實例的Close()方法處等待DataReceived事件處理程序執行完成。
此時,線程死鎖發生,兩邊都執行不下去了。

解決死鎖

網上大多數方法都是定義2個bool類型的標記Listening和Closing,關閉串口和接受數據前先判斷一下。
我的方法是DataReceived事件處理程序用this.BeginInvoke()更新界面,不等待UI線程執行完委托就返回,stream的線程鎖會很快釋放,SerialPort實例的Close()方法也無需等待。

總結

問題最終的答案其實很簡單,但我在查閱.NET源碼查找問題源頭的過程中收獲了很多。這是我第一次這么深入的查看.NET源碼,發現這種解決問題的方法還是很有用處的。結果不重要,解決問題的方法是最重要的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM