程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> 多線程與Invoke,Beginvoke

多線程與Invoke,Beginvoke

編輯:C#基礎知識

首先,什麼樣的操作需要考慮使用多線程?

      總的一條就是,負責與用戶交互的線程(以下簡稱為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線程輪詢(Polling)該對象並反饋給用戶就可以了。不過,這還是挺麻煩的,實際上不用這樣做,Invoke 和BeginInvoke 就是為了解決這個問題而出現的,使你在多線程中安全的更新界面顯示。

     Control類(及其派生類)對象有一個Invoke方法很特別,這是少數幾個不受線程限制的成員之一。我們前面說到,絕對不要在任何其他線程裡面調用非本線程創建的控件的成員時,也說了“只有極個別情況例外”,這個Invoke方法就是極個別情況之一----Invoke方法可以從任何線程裡面調用。

    下面我們來講解 Invoke方法。Invoke方法的參數很簡單,一個委托,一個參數表(可選),而Invoke方法的主要功能就是幫助你在UI線程(即創建控件的線程)上調用委托所指定的方法。

    Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,如果是,直接執行委托指向的方法,如果不是,它將切換到UI線程,然後執行委托指向的方法。不管當前線程是不是UI線程,Invoke都阻塞直到委托指向的方法執行完畢,然後切換回發出調用的線程(如果需要的話),返回。注意,使用Invoke方法時,UI線程不能處於阻塞狀態。

    好了,說完Invoke,說說BeginInvoke,毫無疑問這是Invoke的異步版本 (Invoke是同步完成的),不過大家不要和上面的System.Windows.Forms.MethodInvoker委托中的 BeginInvoke混淆,兩者都是利用不同線程來完成工作,但是控件的BeginInvoke方法總是使用UI線程,而其他的異步委托調用方法則是利用線程池裡的線程。相對Invoke而言,使用BeginInvoke稍稍麻煩一點,但還是那句話,異步比同步效果好,盡管復雜些。比如同步方法可能出現這樣一種死鎖情況:工作者線程通過Invoke同步調用UI線程裡的方法時會阻塞,而萬一UI線程正在等待工作者線程做某件事時怎麼辦?因此,能夠使用異步方法時應盡量使用異步方法。

例1:

// 這是由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!";
}

例2:

        public delegate void MyInvoke(string str);

private void btnEnter_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(DoWord));
thread.Start();
}

public void DoWord()
{
MyInvoke mi = new MyInvoke(SetTxt);
BeginInvoke(mi, new object[] { "abc" });
}

public void SetTxt(string str)
{
txtContent.Text = str;
}
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved