【WPF】後臺線程(包括串口等設備線程)安全的訪問前臺UI元素

問題描述

寫的一個程序,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# 線程同步的多種方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章