當我們需要處理大量數據時,為了使UI界面不致出現假死狀態,我們就必須使用多線程進行處理。所以問題就出現了,我們都知道線程作為一個獨立運行的單元,線程間不可以隨意訪問和修改,那麼該怎麼辦呢?其實C#提供了跨線程訪問的方法,也就是通過委託安全調用從非擁有控件的線程訪問控件。
一、委託
我們首先先來瞭解下委託,簡單地說,委託就是一個類,它定義了方法傳遞參數的類型和個數,使得我們可以把方法作為參數進行傳遞,使得程序具有更好的擴展性。如果大家還不明白的話,我們可以舉個例子:
- private delegate void setTextDelegate( string msg); //聲明一個委託類型,這個委託類型傳遞一個string類型參數,並返回為void
- private void setLabelText(string value) //這是一個傳遞string類型參數,並返回void的方法
- {
- this.label1.Text = value;
- }
- private void setTextboxTex(string msg) //這也是一個傳遞string類型參數,並返回void的方法
- {
- this.textBox1.Text = msg;
- }
- private void setText(string msg,setTextDelegate std) //把setTextDelegate類型委託作為一個參數進行傳遞
- {
- std(msg); //真正的參數傳遞
- }
- private void button1_Click(object sender, EventArgs e)
- {
- this.setText("這是label控件", setLabelText); //因為setLabelText傳遞的參數類型和返回值都和委託聲明的類型一致,所以可以進行傳遞
- this.setText("這是textBox控件", setTextboxTex); //同上
- }
總結下,從上面的例子我們可以清楚地看到,只要方法傳遞的參數類型、個數和返回值與委託聲明的一致,我們就可以把該方法作為參數進行傳遞。
我們也可以將多個方法綁定到委託上,所有綁定上去的方法就會形成一個鏈表順序執行。
- private void button1_Click(object sender, EventArgs e)
- {
- setTextDelegate std = new setTextDelegate(setLabelText); //實例化委託並綁定setLabelText方法
- std += setTextboxTex; //使用"+="號綁定方法,解綁使用"-="號
- this.setText("這是控件", std);
- }
這樣setTextDelegate綁定了兩個方法,當我們this.setText("這是控件", std);時兩個方法會順序執行,這樣做大大提高了程序的可擴性。
注意:委託並沒有提供0參數的構造函數,所以setTextDelegate std = new setTextDelegate(); std += setLabelText; std += setTextboxTex; 這樣編譯會出錯。二、線程安全及委託調用
在講解跨線程委託調用的方法前,我們先瞭解幾個常用的方法和屬性:
1.Control.InvokeRequired屬性(Control是所有控件的基類),這個屬性用來判斷Control控件是否為調用線程創建的,如果為否的話,也就是創建Control控件的線程不是調用線程,返回false,否則返回true。
2.Control.Invoke() 方法,這是同步調用的方法,它順序執行Invoke(Delegate)裡的委託方法會再繼續執行下面的方法,下面會詳細解釋。
3.Control.BeginInvoke()方法,這是異步調用的方法,它會類似於把委託內的方法又創建了一條線程來執行,只有調用線程進行Sleep切換時才會執行委託方法,下面會詳細解釋它和Control.Invoke()方法的區別。
先來看個例子:
- private delegate void setTextDelegate( int value); //先聲明一個傳遞int類型參數,並返回為void的委託
- private void button1_Click(object sender, EventArgs e)
- {
- Thread newThread = new Thread(new ThreadStart(threadHandler));
- newThread.Start();
- }
- private void threadHandler()
- {
- for(int i =0 ; i <=100 ; i ++)
- {
- this.UIHandler(i);
- Thread.Sleep(100);
- }
- }
- private void UIHandler(int value)
- {
- if(this.label1.InvokeRequired) //判斷label1控件是否是調用線程(即newThread線程)創建的,也就是是否跨線程調用,如果是則返回true,否則返回false
- {
- this.label1.BeginInvoke(new setTextDelegate(setLabelText),new object []{ value}); //異步調用setLabelText方法,並傳遞一個int參數
- }
- else
- {
- this.label1.Text = value.ToString() + "%";
- }
- }
- private void setLabelText(int value) //當跨線程調用時,調用該方法進行UI界面更新
- {
- this.label1.Text = value.ToString() + "%";
- }
這是一個簡單的跨線程調用的例子,不懂的可以把例子拷貝自己運行下就清楚了,其實原理很簡單,分兩步就搞定了:
1. 聲明一個委託類型,定義它需要傳遞的參數類型、個數和返回的類型。
2.判斷是否跨線程調用更新控件內容(Control.InvokeRequired),如果是的話,就要用Invoke()或BeginInvoke()執行委託。
三、Control.Invoke()與Control.BeginInvoke()方法的區別
我們都知道這兩個方法都可以進行委託執行,但其中Control.Invoke()是同步調用執行,Control.BeginInvoke()是異步調用執行。
MSDN上的解釋是這樣的:
Control.Invoke()方法:在擁有此控件的基礎窗口句柄的線程上執行委託。
Control.BeginInvoke()方法:在創建控件的基礎句柄所在線程上異步執行委託。
從MSDN的解釋我們可以得出,委託都是在Control線程執行的,也就是UI主線程執行的。兩者的區別主要是異步執行和順序執行的區別,也就是執行順序的不一致。
下面舉個例子來看就明白了,就拿上面的例子來講解好了。
- private delegate void setTextDelegate( int value); //先聲明一個傳遞int類型參數,並返回為void的委託
- private void button1_Click(object sender, EventArgs e)
- {
- Thread newThread = new Thread(new ThreadStart(threadHandler));
- newThread.Start();
- }
- private void threadHandler()
- {
- for(int i =0 ; i <=100 ; i ++)
- {
- this.UIHandler(i);
- Thread.Sleep(100);
- }
- }
- private void UIHandler(int value)
- {
- // ........①
- if(this.label1.InvokeRequired) //判斷label1控件是否是調用線程(即newThread線程)創建的,也就是是否跨線程調用,如果是則返回true,否則返回false
- {
- this.label1.Invoke(new setTextDelegate(setLabelText),new object []{ value}); //同步調用......②
- //this.label1.BeginInvoke(new setTextDelegate(setLabelText),new object []{ value}); //異步調用 ......③
- }
- else
- {
- this.label1.Text = value.ToString() + "%"; //.......④
- }
- //.........⑤
- }
- private void setLabelText(int value) //當跨線程調用時,調用該方法進行UI界面更新
- {
- this.label1.Text = value.ToString() + "%"; //..........⑥
- }
正確的執行順序:Invoke是①->②->⑥-->⑤,而BeginInvoke是①->③->⑤->⑥,④這步不會執行到。
大家可以自己去試下,其實原理我上面已經說過了,使用Invoke方法的話,調用線程會馬上進行切換操作,切換到Control控件所在線程(UI線程)進行界面的更新後,再切換回來繼續執行下面的內容,而BeginInvoke方法的話,調用線程不會馬上進行切換操作,它會在Thread.Sleep(100);的時候進行切換到UI線程進行界面的更新,所以用BeginInvoke方法的時候一定要注意,每次執行完要Thread.Sleep(100);下讓界面更新,否則界面還是不會有變化的。
四、委託的類型
.Net Framework 提供了兩種類型的委託,一種是自定義型的,一種是系統定義型的(固定類型)。自定義型的就不說了,也就是需要自己進行聲明委託類型,靈活性高,不過系統默認提供的幾個固定類型的委託也可以讓我們快速實現委託。
1. public delegate void MethodInvoker () //聲明返回值為 void 且不接受任何參數傳遞的任何方法
- private void button1_Click(object sender, EventArgs e)
- Thread newThread = new Thread(new ThreadStart(threadHandler));
- newThread.Start();
- }
- private void threadHandler()
- {
- if(this.label1.InvokeRequired) //判斷label1控件是否是調用線程(即newThread線程)創建的,也就是是否跨線程調用,如果是則返回true,否則返回false
- {
- this.label1.Invoke(new MethodInvoker(threadHandler)); //把封包發送UI線程,即UI線程執行threadHandler方法
- }
- else
- {
- this.label1.Text = "這是lable控件"; //第二次this.label1.InvokeRequired判斷後會執行到這裡
- }
- }
- private void button1_Click(object sender, EventArgs e)
- Thread newThread = new Thread(new ThreadStart(threadHandler));
- newThread.Start();
- }
- private void threadHandler()
- {
- if(this.label1.InvokeRequired) //判斷label1控件是否是調用線程(即newThread線程)創建的,也就是是否跨線程調用,如果是則返回true,否則返回false
- {
- this.label1.Invoke(new EventHandler(setLabelText),new object[]{"這是lable控件"}); //EventHandler可以傳遞object參數
- }
- else
- {
- this.label1.Text = "這是lable控件";
- }
- }
- private void setLabelText(object o, System.EventArgs e)
- {
- this.label1.Text = o.ToString();
- }