很多人對Invoke和BeginInvoke理解不深刻,不知道該怎么應用,在這篇博文里將詳細闡述Invoke和BeginInvoke的用法:
首先說下Invoke和BeginInvoke有兩種用法:
1.Control中Invoke,BeginInvoke
2.Delegate中Invokke,BeginInvoke
這兩種情況是不同的,我們首先講一下第一種,也就是Control類中的Invoke,BeginInvoke的用法。我們先來看一下MSDN是如何解釋的:
control.invoke(參數delegate)方法:在擁有此控件的基礎窗口句柄的線程上執行指定的委托。
control.begininvoke(參數delegate)方法:在創建控件的基礎句柄所在線程上異步執行指定委托。
通過官方的解釋,我們大概覺得 Invoke是同步的,而BeginInvoke是異步的,至於兩者的實際差別,我們通過代碼來展示:
Thread invokeThread; public delegate void invokeDelegate(); private void button1_Click(object sender, EventArgs e) { //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "AAA"); Console.WriteLine("AAA"); invokeThread = new Thread(new ThreadStart(StartMethod)); invokeThread.Start(); string a = string.Empty; for (int i = 0; i < 3; i++) //調整循環次數,看的會更清楚 { Thread.Sleep(1000); a = a + "B"; } // MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + a); Console.WriteLine(a); } private void StartMethod() { // MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "CCC"); Console.WriteLine("CCC"); button1.Invoke(new invokeDelegate(invokeMethod)); //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "DDD"); Console.WriteLine("DDD"); Console.Read(); } private void invokeMethod() { //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "EEE"); Console.WriteLine("EEE"); }
運行結果為:

從結果可以看出執行時首先執行AAA,然后CCC和BBB同時執行,Invoke將Invoke方法提交給主線程,主線程執行完運行在Invoke上的方法,然后才執行子線程的方法,簡單講同步就是說必須等帶Invoke上的方法執行完,才能執行子線程的方法。
再來看看BeginInvokke的執行邏輯:
Thread invokeThread; public delegate void invokeDelegate(); private void button1_Click(object sender, EventArgs e) { //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "AAA"); Console.WriteLine("AAA"); invokeThread = new Thread(new ThreadStart(StartMethod)); invokeThread.Start(); string a = string.Empty; for (int i = 0; i < 3; i++) //調整循環次數,看的會更清楚 { Thread.Sleep(1000); a = a + "B"; } // MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + a); Console.WriteLine(a); } private void StartMethod() { // MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "CCC"); Console.WriteLine("CCC"); button1.BeginInvoke(new invokeDelegate(invokeMethod)); //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "DDD"); Console.WriteLine("DDD"); Console.Read(); } private void invokeMethod() { //MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "EEE"); Console.WriteLine("EEE"); }
運行的結果為:

從結果可以看出,首先執行AAA,然后CCC和BBB同時執行,由於采用了BeginInvoke方法,即所謂的異步執行,子線程不再等待主線程執行完成,所以DDD輸出,主線程執行完BBB再執行EEE.
從運行的結果來看不管時Invoke提交的方法,還是BeginInvoke提交的方法,都是在主線上執行的,所以同步和異步的概念是相對於子線程來說的,在invoke例子中我們會發現invoke所提交的委托方法執行完成后,才能繼續執行 DDD;在begininvoke例子中我們會發現begininvoke所提交的委托方法后,子線程講繼續執行DDD,不需要等待委托方法的完成。 那么現在我們在回想下invoke(同步)和begininvoke(異步)的概念,其實它們所說的意思是相對於子線程而言的,其實對於控件的調用總是由 主線程來執行的。我們很多人搞不清這個同步和異步,主要還是因為我們把參照物選錯了。其實有時候光看概念是很容易理解錯誤的,Invoke會阻塞主支線程,BeginInvoke只會阻塞主線程,不會阻塞支線程!因此BeginInvoke的異步執行是指相對於支線程異步,而不是相對於主線程異步 。現在是不是徹底搞清楚兩者的區別啦。
解決不是從創建控件的線程訪問它
在多線程編程中,我們經常要在工作線程中去更新界面顯示,而在多線程中直接調用界面控件的方法是錯誤的做法,Invoke 和 BeginInvoke 就是為了解決這個問題而出現的,使你在多線程中安全的更新界面顯示。
正確的做法是將工作線程中涉及更新界面的代碼封裝為一個方法,通過 Invoke 或者 BeginInvoke 去調用,兩者的區別就是一個導致工作線程等待,而另外一個則不會。
由於歷史原因,形成了以下幾種寫法:
//第一種 if (this.textBox1.InvokeRequired) { this.textBox1.Invoke(new EventHandler( delegate { this.textBox1.Text = "測試"; })); } //第二種 this.Invoke(new EventHandler(delegate { this.textBox1.Text = "測試"; })); //第三種 this.Invoke(new Action(() => { this.textBox1.Text = "測試"; }));
毫無疑問,第三種調用方法是最佳選擇。
delegate中的Invoke方法和BeginInvoke方法
delegate中Invoke方法和BenginInvoke方法主要是用在同步調用,異步調用,異步回調上
A.同步調用,舉個簡單的例子:
private void button2_Click(object sender, EventArgs e) { Add d = new Add((a, b) => { Thread.Sleep(5000); return a + b; }); //實例化委托並給委托賦值 int result = d.Invoke(2, 5); Console.WriteLine("繼續做別的事情"); Console.WriteLine(result.ToString()); Console.Read(); } public delegate int Add(int i1,int i2);
Invoke方法的參數很簡單,一個委托,一個參數表(可選),而Invoke方法的主要功能就是幫助你在UI線程上調用委托所指定的方法。Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,如果是,直接執行委托指向的方法,如果不是,它將切換到UI線程,然后執行委托指向的方法。不管當前線程是不是UI線程,Invoke都阻塞直到委托指向的方法執行完畢,然后切換回發出調用的線程(如果需要的話),返回。
所以Invoke方法的參數和返回值和調用他的委托應該是一致的
B.異步調用
舉個簡單的例子:
public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { //委托類型的Begininvoke(<輸入和輸出變量>,AsyncCallbac callback,object asyncState)方法:異步調用的核心 //第一個參數10,表示委托對應的方法實參。 //第二個參數callback,回調函數,表示異步調用結束時自動調用的函數。 //第三個參數asyncState,用於向回調函數提供相關參數信息 //返回值:IAsyncResult -->異步操作狀態接口,封裝了異步執行的中的參數 //IAsyncResult接口成員:查看幫助文檔 Calcute objCalcute = new Calcute(ExcuteDelegate1); IAsyncResult result = objCalcute.BeginInvoke(1, 2, null, null); //IAsyncResult是一個接口 this.textBox1.Text = "等待結果......."; this.textBox2.Text = ExcuteDelegate2(20, 10).ToString();
//委托類型的EndInvoke()方法:借助於IAsyncResult接口對象,不斷查詢異步調用是否結束。
//該方法知道被異步調用的方法所有參數,所有,異步調用完畢后,取出異步調用結果作為返回值。
this.textBox1.Text = objCalcute.EndInvoke(result).ToString(); } public delegate int Calcute(int i1, int i2); int ExcuteDelegate1(int i1, int i2) { Thread.Sleep(5000); return i1 + i2; } int ExcuteDelegate2(int i1, int i2) { return i1 * i2; } }
從運行結果可以看出運行BeginInvoke上運行的方法時不會影響 this.textBox2.Text = ExcuteDelegate2(20, 10).ToString(); 的執行,但是EndInvoke方法需要等待線程執行完畢,所以依舊會阻塞主線程的執行,為了解決這個問題,異步回調就出現了,其實可以將異步執行看成是異步回調的特殊情況
C.異步回調
看下面的例子:
public FrmCalllBack() { InitializeComponent(); //【3】初始化委托變量 this.objMyCal = new MyCalculator(ExecuteTask); //也可以直接使用Lambda表達式 this.objMyCal = (num, ms) => { System.Threading.Thread.Sleep(ms); return num * num; }; } //【3】創建委托變量(因為異步函數和回調函數都要用,所以定義成員變量) private MyCalculator objMyCal = null; //【1】聲明一個委托 public delegate int MyCalculator(int num, int ms); /// <summary> /// 【2】根據委托定義一個方法:返回一個數的平方 /// </summary> /// <param name="num">基數</param> /// <param name="ms">延遲的時間:秒</param> /// <returns></returns> private int ExecuteTask(int num, int ms) { System.Threading.Thread.Sleep(ms); return num * num; } //【4】同時執行多個任務 private void btnExec_Click(object sender, EventArgs e) { for (int i = 1; i < 11; i++)//產生10個任務 { //開始異步執行,並封裝回調函數 objMyCal.BeginInvoke(10 * i, 1000 * i, null, i); objMyCal.BeginInvoke(10 * i, 1000 * i, MyCallBack, i); //最后一個參數 i 給回調函數的字段AsyncState賦值,如果數據很多可以定義成類或結構 } } //【5】回調函數 private void MyCallBack(IAsyncResult result) { int res = objMyCal.EndInvoke(result); //異步顯示結果:result.AsyncState字段用來封裝回調時自定義的參數,object類型 Console.WriteLine("第{0}個計算結果為:{1}", result.AsyncState.ToString(), res); } }
從運行結果看出,異步回調不再阻塞主線程的執行。注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,但EndInvoke還是必須調用,否則可能會造成內存泄漏。
異步編程的總結:
1. 異步編程是建立在委托基礎上的一種編程方法。
2. 異步調用的每個方法都是在獨立的線程中執行。因此,本質上就是一種多線程程序,也可以說是一種“簡化版本”的多線程技術。
3. 比較適合在后台運行較為耗費時間的"簡單任務",並且任務要求相互獨立,任務中不應該有代碼直接訪問可視化控件。
4. 如果后台任務要求必須按照特定順序執行,或者必須訪問共享資源,則異步編程不適合,而應該直接采用多線程開發技術。
