目錄
- 問題描述
- 查找原因
- SerialPort類Open()方法
- SerialPort類Close()方法
- 死鎖原因
- 解決死鎖
- 總結
問題描述
前幾天用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原始碼,發現這種解決問題的方法還是很有用處的,結果不重要,解決問題的方法是最重要的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/143507.html
標籤:其他
