問題描述
寫的一個程序,WPF界面,與串口、相機等硬件設備通信,並將通信結果(包括圖片等)顯示在UI界面上。期間發現有串口被其他線程佔用,在Debug模式下關閉程序後,程序無法退出等情況。
具體表現爲:
(1)關閉程序後,下次再次啓動程序,串口被佔用,新的程序無法訪問串口;表現爲報串口異常:
“Access to the port ‘COM8’ is denied.
An exception of type System.UnauthorizedAccessException occurred and was caught.”
(2)有時候,關閉上一個程序後,程序未能完全退出:
(a)一種表現爲雖然界面都關閉了,但是進程管理器中還有;
(b)另一種表現爲,debug模式下運行程序。當把程序關閉後,VS還一直處於調試狀態,無法退出程序,表示當前程序還在運行,VS下暫停程序,顯示程序正在運行相機線程的銷燬中,正在以下語句中等待,表明線程一直無法退出。
m_monitorThread.Join();
問題探索
隨後,在網上搜索了類似情況。比較有用的有:
【1】C# 串口操作系列(2) – 入門篇,爲什麼我的串口程序在關閉串口時候會死鎖
其中說的是普通的UI的操作情況。
隨後,對我的程序進行了分析,發現也存在類似情況。於是進行了改進。
在上面的文章有分析,這裏結合我的程序,這裏簡單說下:
(1)串口線程、相機線程單獨工作,當有消息時,他們會回調函數進行界面UI元素的更新。在我的程序中,圖像的更新頻率很高,20幀/秒。串口回調也比較頻繁。
這裏有兩種更新方式:
(a)直接更新UI控件元素,例如圖片的ImgSrc。
(b)更新綁定了前端顯示控件的屬性。
這兩種具體更新的機制不是十分清除,但估計也是需要等待UI,等待wpf的鎖定資源才能訪問。
(2)當關閉主程序時,要進行串口、相機的關閉和資源的釋放時。此時,對其關閉需要等待其線程釋放相關的資源纔行。
上述兩個過程中(1)是後臺線程等主線程;(2)是主線程等待後臺,就會發生死鎖。
死鎖的結果:
(1)上一個程序關閉時無法正常釋放串口資源,使得再次打開的程序無法正常打開串口,出現異常“Access to the port ‘COM8’ is denied.
An exception of type System.UnauthorizedAccessException occurred and was caught.”
(2)有串口或相機線程無法釋放時,則問題中的(2)情況發生。
更改方案
有了以上的情況,下面的關鍵就是要在訪問時增加同步及程序判斷。在上面的文章中說了一種方法,此外,還可以利用線程同步的方式,如ManualResetEvent 類等。具體線程同步的介紹可以參見:
C# 線程同步的多種方式
本文用了引文【1】中的處理方式。
具體來說:
(1) 設置兩個變量,表明誰在忙,wpf的訪問與普通c#中不太一樣。具體見代碼,封裝的相關函數等。
(2) 在引用1中,主程序使用這個Application.DoEvents()來等待。而在wpf使用了這個:
System.Windows.Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background, new System.Threading.ThreadStart(delegate { }));
(3)爲了便於公用的訪問,這裏進行了封裝。
具體代碼如下:
/// <summary>
/// 表徵當前線程與另一個線程互相資源調用依賴關係,避免死鎖
/// </summary>
protected bool m_isUpdatingUiThread = false; //當前線程可能會訪問Ui元素時爲真
protected bool m_isClosingCurrentThread = false; //被主線程進行銷燬關閉操作時
///訪問UI的代碼示例。這裏封裝了一個函數
/// <summary>
/// 當前線程訪問UI的安全Action。避免在訪問UI時,UI線程關閉當前線程,造成死鎖
/// 使用方式:可以將需要訪問UI的代碼封裝爲Action傳入
/// </summary>
/// <param name="act"></param>
/// <returns></returns>
protected bool UISafeAccessAction(Action act)
{
if (!CanAccessUi()) return false;
try
{
act.Invoke();
}
catch (Exception ex)
{
//異常處理....
}
finally
{
FinallyAccessUI();
}
return true;
}
/// <summary>
/// 釋放線程、相機等資源
/// </summary>
public override void Dispose()
{
Prepare2CloseCurrentThread();
//實際的關閉和close動作...
AfterFinishCloseCurrentThread();
}
protected bool CanAccessUi()
{
if (m_isClosingCurrentThread) return false;
m_isUpdatingUiThread = true;
return true;
}
/// <summary>
/// 訪問完本線程的釋放後,一定要執行本函數置位。即使是出異常了也移動要執行本函數。
/// !!!!!必須將本函數防止在try後面的finally中!!!!!
/// </summary>
protected void FinallyAccessUI()
{
m_isUpdatingUiThread = false;
}
///關閉前兩個函數:
/// <summary>
/// 主程序在試圖關閉當前線程或釋放當前線程資源時,需調用該方法,保證當前線程沒有訪問主線程界面中的東西
/// 如果正在訪問,將會阻塞等待。否則直接訪問
/// 注意:這裏只能防止當前線程中只能順序的對UI進行訪問,不能支持多線程都在訪問UI資源的同步!!!!
/// </summary>
protected void Prepare2CloseCurrentThread()
{
m_isClosingCurrentThread = true;
while (m_isUpdatingUiThread) System.Windows.Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background, new System.Threading.ThreadStart(delegate { }));
}
/// <summary>
/// 主線程對當前線程關閉或釋放完資源後,調用該函數置位標記
/// </summary>
protected void AfterFinishCloseCurrentThread()
{
m_isClosingCurrentThread = false;
}
(4)需要說明的是,由於使用的是一個變量來,這裏只能防止當前線程中只能順序的對UI進行訪問,不能支持多線程都在訪問UI資源的同步!!!!如果是多個的UI訪問的話,可以設置多個變量,對多個變量的狀態進行控制後進行關閉!
引用文章:
【1】C# 串口操作系列(2) – 入門篇,爲什麼我的串口程序在關閉串口時候會死鎖
【2】C# 線程同步的多種方式