invoke和begininvoke 區別
一直對invoke和begininvoke的使用和概念比較混亂,這兩天看了些資料,對這兩個的用法和原理有了些新的認識和理解。
首先說下,invoke和begininvoke的使用有兩種情況:
-
control中的invoke、begininvoke。
-
delegrate中的invoke、begininvoke。
這兩種情況是不同的,我們這裡要講的是第1種。下面我們在來說下.NET中對invoke和begininvoke的官方定義。
control.invoke(引數delegate)方法:在擁有此控制元件的基礎視窗控制代碼的執行緒上執行指定的委託。
control.begininvoke(引數delegate)方法:在建立控制元件的基礎控制代碼所線上程上非同步執行指定委託。
根據這兩個概念我們大致理解invoke表是同步、begininvoke表示非同步。但是如何來進行同步和非同步呢?我們來做一個測試。
invoke 例子:
private void button1_Click(object sender, EventArgs e) { MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"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); } private void StartMethod() { MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"CCC"); button1.Invoke(new invokeDelegate(invokeMethod)); MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"DDD"); } private void invokeMethod() { //Thread.Sleep(3000); MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "EEE"); }
結論:我們執行後,看下程式的執行順序,1AAA->3CCC和1BBB->1EEE ->3DDD 。
解釋:主執行緒執行1AAA,然後1BBB和子執行緒3CCC同時執行,然後通過invoke來將invokemethod方法提交給主執行緒,然後子線 程等待主執行緒執行,直到主執行緒將invokemethod方法執行完成(期間必須等待主執行緒的任務執行完成,才會去執行invoke提交的任務),最後執 行子執行緒3DDD。
begininvoke 例子:
private void button1_Click(object sender, EventArgs e) { MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"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); } private void StartMethod() { MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"CCC"); button1.BeginInvoke(new invokeDelegate(beginInvokeMethod)); MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString()+"DDD"); } private void beginInvokeMethod() { //Thread.Sleep(3000); MessageBox.Show(Thread.CurrentThread.GetHashCode().ToString() + "EEEEEEEEEEEE"); }
結論: 我們執行後看看執行的結果:1AAA->1BBB和3CCC->1EEE和3DDD。
解釋: 主執行緒執行1AAA,然後1BBB和子執行緒3CCC同時執行,然後通過begininvoke來將invokemethod方法提交給主執行緒,然後主執行緒執行1EEE(主執行緒自己的任務執行完成), 同時子執行緒繼續執行3DDD。
通過這個兩段程式碼的測試比較,我們會發現其實invoke和begininvoke所提交的委託方法都是在主執行緒中執行的,其實根據我invoke 和begininvoke的定義我們要在子執行緒中來看這個問題,在invoke例子中我們會發現invoke所提交的委託方法執行完成後,才能繼續執行 DDD;在begininvoke例子中我們會發現begininvoke所提交的委託方法後,子執行緒講繼續執行DDD,不需要等待委託方法的完成。 那麼現在我們在回想下invoke(同步)和begininvoke(非同步)的概念,其實它們所說的意思是相對於子執行緒而言的,其實對於控制元件的呼叫總是由 主執行緒來執行的。我們很多人搞不清這個同步和非同步,主要還是因為我們把參照物選錯了。其實有時候光看概念是很容易理解錯誤的。
**解決從不是建立控制元件的執行緒訪問它
在多執行緒程式設計中,我們經常要在工作執行緒中去更新介面顯示,而在多執行緒中直接呼叫介面控制元件的方法是錯誤的做法,Invoke 和 BeginInvoke 就是為了解決這個問題而出現的,使你在多執行緒中安全的更新介面顯示。
正確的做法是將工作執行緒中涉及更新介面的程式碼封裝為一個方法,通過 Invoke 或者 BeginInvoke 去呼叫,兩者的區別就是一個導致工作執行緒等待,而另外一個則不會。
而所謂的“一面響應操作,一面新增節點”永遠只能是相對的,使 UI 執行緒的負擔不至於太大而已,因為介面的正確更新始終要通過 UI 執行緒去做,我們要做的事情是在工作執行緒中包攬大部分的運算,而將對純粹的介面更新放到 UI 執行緒中去做,這樣也就達到了減輕 UI 執行緒負擔的目的了。**
舉個簡單例子說明下使用方法,比如你在啟動一個執行緒,線上程的方法中想更新窗體中的一個TextBox…
using System.Threading;
//啟動一個執行緒
Thread thread=new Thread(new ThreadStart(DoWork));
thread.Start();
//執行緒方法
private void DoWork()
{
this.TextBox1.Text="我是一個文字框";
}
如果你像上面操作,在VS2005或2008裡是會有異常的…
正確的做法是用Invoke\BeginInvoke
using System.Threading;
namespace test
{
public partial class Form1 : Form
{
public delegate void MyInvoke(string str1,string str2);
public Form1()
{
InitializeComponent();
}
public void DoWork()
{
MyInvoke mi = new MyInvoke(UpdateForm);
this.BeginInvoke(mi, new Object[] {"我是文字框","haha"});
}
public void UpdateForm(string param1,string parm2)
{
this.textBox1.Text = param1+parm2;
}
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
}
}
}
注意代理的使用!
後面再次補充
在 WinForm開發過程中經常會用到執行緒,有時候還往往需要線上程中訪問執行緒外的控制元件,比如:設定textbox的Text屬性等等。如果直接設定程式必 定會報出:從不是建立控制元件的執行緒訪問它,這個異常。通常我們可以採用兩種方法來解決。一是通過設定control的屬性。二是通過delegate,而通 過delegate也有兩種方式,一種是常用的方式,另一種就是匿名方式。下面分別加以說明.
首先,通過設定control的一個屬性值為false.我們可以在Form_Load方法中新增:Control.CheckForIllegalCrossThreadCalls=false;來解決。設定為false表示不對錯誤執行緒的呼叫進行捕獲。這樣線上程中對textbox的Text屬性進行設定時就不會再報錯了。
其次,通過delegate的方法來解決。
普通的委託方法例如:
delegate void SafeSetText(string strMsg);
private void SetText(string strMsg)
{
if(textbox1.InvokeRequired)
{
SafeSetText objSet=new SafeSetText(SetText);
textbox1.Invoke(objSet,new object[]{strMsg});
}
else
{
textbox1.Text=strMsg;
}
}
線上程內需要設定textbox的值時呼叫SetText方法既可。我們還可以採用另一種委託的方式來實現,那就是匿名代理,例如:
delegate void SafeSetText(string strMsg);
private void SetText2(string strMsg)
{
SafeSetText objSet = delegate(string str)
{
textBox1.Text = str;
}
textBox1.Invoke(objSet,new object[]{strMsg});
}
這樣同樣可以實現。
個人覺得還是採用代理好些。
在C# 3.0及以後的版本中有了Lamda表示式,像上面這種匿名委託有了更簡潔的寫法。.NET Framework 3.5及以後版本更能用Action封裝方法。例如以下寫法可以看上去非常簡潔:
void ButtonOnClick(object sender,EventArgs e)
{
this.Invoke(new Action(()=>
{
button.Text="關閉";
}));
}