閱讀145 返回首頁    go 阿裏雲 go 技術社區[雲棲]


C# WinForm多線程開發(三) Control.Invoke

原文地址:點擊打開鏈接

[摘要]本文介紹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!";
}
第五,關於多線程編程還要考慮線程之間的同步問題、死鎖和爭用條件,有關這類問題的文章很多,我們就不贅述了

最後更新:2017-04-03 16:49:20

  上一篇:go 地圖開發添加標注物
  下一篇:go linux係統下載 僅供學習測試