原文地址:點擊開啟連結
[摘要]本文介紹C# WinForm多線程開發之Control.Invoke,並提供詳細的範例程式碼供參考。
下面我們就把在Windows Form軟體中使用Invoke時的多線程要注意的問題給大家做一個介紹。
首先,什麼樣的操作需要考慮使用多線程?總的一條就是,負責與使用者互動的線程(以下簡稱為UI線程)應該保持順暢,當UI線程調用的API可能引起阻塞時間超過30毫秒時(比如訪問CD-ROM等速度超慢的外設、進行遠程調用等等)就應該考慮使用多線程。為什麼是30毫秒?30毫秒的概念是人眼可以察覺到的一個遲滯,大約等同於電影裡的一幀停留的時間,最長不要超過100毫秒。
第二,最方便和簡單的多線程是使用線程池。通過線程池裡的線程運行代碼的最簡便方法則是使用非同步委託調用。注意委託調用通常是同步完成的,請使用BeginInvoke方法,這樣就可以把要調用的方法排隊到線程池裡等候處理,而程式的流程會立刻返回到調用方(此處是UI線程),而調用方因此不會出現阻塞。
看看下面的例子我們就發現要使用線程池非同步執行代碼也並非十分複雜,這裡我們利用System.Windows.Forms.MethodInvoker委託進行非同步呼叫。注意MethodInvoker委託不接受方法參數,如果需要向非同步執行的方法傳遞參數,請使用其他委託,或者需要自己定義。
private void StartSomeWorkFromUIThread () { // 我們要做的工作相對UI線程而言台慢了,用下面的方法非同步進行處理 MethodInvoker mi = new MethodInvoker(RunsOnWorkerThread);//這是入口方法 mi.BeginInvoke(null, null); // 這樣就不會阻塞}// 緩慢的工作在此方法內進行處理,使用線程池裡的線程private void RunsOnWorkerThread() { DoSomethingSlow();}
歸納上述方法,對UI線程而言實際上就是:1、發出調用,2、立刻返回,具體運行過程不理了,這樣UI線程就不會被阻塞。這種方法很重要,下面我們會深入介紹。除了上面的方法,還有其他使用線程池的方法,當然如果你高興也可以自己建立線程。
第三,在Windows Form中使用多線程的,最重要的一條注意事項是,除了建立控制項的線程以外,絕對不要在任何其他線程裡面調用控制項的成員(只有極個別情況例外),也就是說控制項屬於建立它的線程,不能從其他線程裡面訪問。這一條適用於所有從System.Windows.Forms.Control派生的控制項(因此可以說是幾乎所有控制項),包括Form控制項本身也是。舉一反三,我們很容易得出這樣的結論,控制項的子控制項必須由建立控制項的線程來建立,比如一個表單上的按鈕,比如由建立表單的線程來建立,因此,一個視窗中的所有控制項實際上都活在同一個線程之中。在實際編程時,大多數的軟體的做法都是讓同一線程負責全部的控制項,這就是我們所說的UI線程。看下面的例子:
// 這是由UI線程定義的Label控制項private Label lblStatus;// 以下方法不在UI線程上執行private void RunsOnWorkerThread() { DoSomethingSlow(); lblStatus.Text = "Finished!"; // 這是錯的}
我們要特別提醒大家,很多人剛開始的時候都會使用以上的方法來訪問不在同一個線程裡的控制項(包括筆者本人),而且在1.0版.Net 架構上似乎沒有發現問題,但是這根本就是錯的,更糟糕的是,程式員在這裡不會得到任何錯誤提示,一開始就上當受騙,之後會莫明其妙地發現其他錯誤,這就是Windows Form多線程編程的痛苦所在。筆者試過花很多時間來Debug自己寫的Splash視窗突然消失的問題,結果還是失敗了:筆者在軟體的引導過程中,用另外一個線程裡建立了一個Splash視窗來顯示歡迎資訊,然後嘗試把主線程裡引導的狀態直接寫入到Splash視窗上的控制項中,開始還OK,可是過一會Splash視窗就莫明其妙消失了。
理解了這一點,我們應該留意到,有時候即使沒有用System.Threading.Thread來顯式建立一個線程,我們也可能因為使用了非同步委託的BeginInvoke方法來隱式建立了線程(從線程池裡),在這種線程裡也同樣不能調用UI線程所建立的控制項的成員。
第四,由於上述限制,我們可能會感到很不方便,的確,當我們利用一個新建立的線程來執行某些花時間的運算時,怎樣知道運算進度如何並通過UI反映給使用者呢?解決方案很多!比如熟悉多線程編程的使用者很快會想到,我們採用一些低級的同步方法,工作者線程把狀態儲存到一個同步對象中,讓UI線程輪詢(Polling)該對象並反饋給使用者就可以了。不過,這還是挺麻煩的,實際上不用這樣做,Control類(及其衍生類別)對象有一個Invoke方法很特別,這是少數幾個不受線程限制的成員之一。我們前面說到,絕對不要在任何其他線程裡面調用非本線程建立的控制項的成員時,也說了“只有極個別情況例外”,這個Invoke方法就是極個別情況之一----Invoke方法可以從任何線程裡面調用。下面我們來講解Invoke方法。
Invoke方法的參數很簡單,一個委託,一個參數表(可選),而Invoke方法的主要功能就是協助你在UI線程(即建立控制項的線程)上調用委託所指定的方法。Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,如果是,直接執行委託指向的方法,如果不是,它將切換到UI線程,然後執行委託指向的方法。不管當前線程是不是UI線程,Invoke都阻塞直到委託指向的方法執行完畢,然後切換回傳出調用的線程(如果需要的話),返回。注意,使用Invoke方法時,UI線程不能處於阻塞狀態。以下MSDN裡關於Invoke方法的說明:
“控制項上有四種方法可以安全地從任何線程進行調用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。對於所有其他方法調用,則應使用調用 (invoke) 方法之一封送對控制項的線程的調用。委託可以是 EventHandler 的執行個體,在此情況下,發送方參數將包含此控制項,而事件參數將包含 EventArgs.Empty。委託還可以是 MethodInvoker 的執行個體或採用 void 參數列表的其他任何委託。調用 EventHandler 或 MethodInvoker 委託比調用其他類型的委託速度更快。”
好了,說完Invoke,順便說說BeginInvoke,毫無疑問這是Invoke的非同步版本(Invoke是同步完成的),不過大家不要和上面的System.Windows.Forms.MethodInvoker委託中的BeginInvoke混淆,兩者都是利用不同線程來完成工作,但是控制項的BeginInvoke方法總是使用UI線程,而其他的非同步委託調用方法則是利用線程池裡的線程。相對Invoke而言,使用BeginInvoke稍稍麻煩一點,但還是那句話,非同步比同步效果好,儘管複雜些。比如同步方法可能出現這樣一種死結情況:工作者線程通過Invoke同步調用UI線程裡的方法時會阻塞,而萬一UI線程正在等待工作者線程做某件事時怎麼辦?因此,能夠使用非同步方法呼叫時應盡量使用非同步方法呼叫。
下面我們利用所學到的知識來改寫上面那個簡單的例子:
// 這是由UI線程定義的Label控制項private Label lblStatus;// 以下方法不在UI線程上執行private void RunsOnWorkerThread() { DoSomethingSlow(); // Do UI update on UI thread object[] pList = { this, System.EventArgs.Empty }; lblStatus.BeginInvoke( new System.EventHandler(UpdateUI), pList);}// 切換回UI線程執行的入口private void UpdateUI(object o, System.EventArgs e) { //現在沒問題了,使用Invoke使得線程總是回到UI線程,所以我們可以放心大膽地調用控制項的成員了 lblStatus.Text = "Finished!";}
第五,關於多線程編程還要考慮線程之間的同步問題、死結和競爭條件,有關這類問題的文章很多,我們就不贅述了
以上就是C# WinForm多線程開發(三) Control.Invoke 的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!