一、BackgroundWorker組件經驗之談
在VS2005中添加了BackgroundWorker組件,該組件在多線程編程方面使用起來非常方便,然而在開始時由於沒有搞清楚它的使用機制,走了不少的彎路,現在把我在使用它的過程中的經驗與諸位分享一下。
BackgroundWorker類中主要用到的有這列屬性、方法和事件:
重要屬性:
1、CancellationPending獲取一個值,指示應用程序是否已請求取消後臺操作。通過在DoWork事件中判斷CancellationPending屬性可以認定是否需要取消後臺操作(也就是結束線程);
2、IsBusy獲取一個值,指示 BackgroundWorker 是否正在運行異步操作。程序中使用IsBusy屬性用來確定後臺操作是否正在使用中;
3、WorkerReportsProgress獲取或設置一個值,該值指示BackgroundWorker能否報告進度更新
4、WorkerSupportsCancellation獲取或設置一個值,該值指示 BackgroundWorker 是否支持異步取消。設置WorkerSupportsCancellation爲true使得程序可以調用CancelAsync方法提交終止掛起的後臺操作的請求;
重要方法:
1、CancelAsync請求取消掛起的後臺操作
2、RunWorkerAsync開始執行後臺操作
3、ReportProgress引發ProgressChanged事件
重要事件:
1、DoWork調用 RunWorkerAsync 時發生
2、ProgressChanged調用 ReportProgress 時發生
3、RunWorkerCompleted當後臺操作已完成、被取消或引發異常時發生
另外還有三個重要的參數是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各屬性、方法、事件的調用機制和順序:
從上圖可見在整個生活週期內發生了3次重要的參數傳遞過程:
參數傳遞1:此次的參數傳遞是將RunWorkerAsync(Object)中的Object傳遞到DoWork事件的DoWorkEventArgs.Argument,由於在這裏只有一個參數可以傳遞,所以在實際應用往封裝一個類,將整個實例化的類作爲RunWorkerAsync的Object傳遞到DoWorkEventArgs.Argument;
參數傳遞2:此次是將程序運行進度傳遞給ProgressChanged事件,實際使用中往往使用給方法和事件更新進度條或者日誌信息;
參數傳遞3:在DoWork事件結束之前,將後臺線程產生的結果數據賦給DoWorkEventArgs.Result一邊在RunWorkerCompleted事件中調用RunWorkerCompletedEventArgs.Result屬性取得後臺線程產生的結果。
另外從上圖可以看到DoWork事件是在後臺線程中運行的,所以在該事件中不能夠操作用戶界面的內容,如果需要更新用戶界面,可以使用ProgressChanged事件及RunWorkCompleted事件來實現。
明白了BagkgroundWorker的事件調用順序和參數傳遞機制之後在使用該組件用於多線程編程的時候就可以輕鬆許多了。
二、異步編程的經典模式和微軟對其的實現
微軟推薦的異步操作模型是事件模型,也即用子線程通過事件來通知調用者自己的工作狀態,也就是設計模式中的observer模式,也可以看成是上文中線程類的擴展,最後實現後調用效果類似於
MyThread thread=new MyThread()
thread.Work+=new ThreadWork(Calculate)
thread.WorkComplete+=new WorkComplete(DisplayResult)
Calculate(object sender, EventArgs e)){
....
}
DisplayResult(object sender, EventArgs e)){
...
}
<例一>
這個話題已經有許多很好的文章,大家參考http://www.cnblogs.com/net66/archive/2005/08/03/206132.html,其作者在文章後附加有示例項目,項目中的線程類實現了事件發送,線程終止,報告任務進度等一系列必要的功能,大家可以自己去查看代碼,我就不贅述了,我主要談微軟對這個模式的實現BackgroundWorker
上篇文章裏說到了控制權的問題,上面的模型在winform下使用有個問題就是執行上下文的問題,在回調函數中(比如<例一>中的DisplayResult中),我們不得不使用BeginInvoke,才能調用ui線程創建的控件的屬性和方法,
比如在上面net66的例子裏
//創建線程對象
_Task = new newasynchui();
//掛接進度條修改事件
_Task.TaskProgressChanged += new TaskEventHandler( OnTaskProgressChanged1 );
//在UI線程,負責更新進度條
private void OnTaskProgressChanged1( object sender,TaskEventArgs e )
{
if (InvokeRequired ) //不在UI線程上,異步調用
{
TaskEventHandler TPChanged1 = new TaskEventHandler( OnTaskProgressChanged1 );
this.BeginInvoke(TPChanged1,new object[] {sender,e});
Console.WriteLine("InvokeRequired=true");
}
else
{
progressBar.Value = e.Progress;
}
}
<例二>
可以看到,在函數裏面用到了
if(InvokeRequired)
{...BeginInvoke....}
else
{....}
這個模式來保證方法在多線程和單線程下都可以運行,所以線程邏輯和界面邏輯混合在了一起,以至把以前很簡單的只需要一句話的任務:progressBar.Value = e.Progress;搞的很複雜,如果線程類作爲公共庫來提供,對編寫事件的人要求會相對較高,那麼有什麼更好的辦法呢?
其實在.Net2.0中微軟自己實現這個模式,製作了Backgroundworker這個類,他可以解決上面這些問題,我們先來看看他的使用方法
System.ComponentModel.BackgroundWorker bw = new System.ComponentModel.BackgroundWorker();
//定義需要在子線程中乾的事情
bw.DoWork += new System.ComponentModel.DoWorkEventHandler(bw_DoWork);
//定義執行完畢後需要做的事情
bw.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
//開始執行
bw.RunWorkerAsync();
static void bw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Complete"+Thread.CurrentThread.ManagedThreadId.ToString());
}
static void bw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
MessageBox.Show(Thread.CurrentThread.ManagedThreadId);
}
<例三>
注意我在兩個函數中輸出了當前線程的ID,當我們在WindowsForm程序中執行上述代碼時,我們驚奇的發現,bw_RunWorkerCompleted這個回調函數居然是運行在UI線程中的,也就是說在這個方法中我們不用再使用Invoke和BeginInvoke調用winform中的控件了, 更讓我奇怪的是,如果是在ConsoleApplication中同樣運行這段代碼,那麼bw_RunWorkerCompleted輸出的線程id和主線程id就並不相同.
那麼BackgroundWorker到底是怎麼實現跨線程封送的呢?
閱讀一下這個類的代碼,我們發現他藉助了AsyncOperation.Post(SendOrPostCallback d, object arg)
在winform下使用這個函數,就可以使得由SendOrPostCallback定義被封送會UI線程,聰明的博友可以用這個方法來實現自己的BackgroundWorker.
繼續查看下去,發現關鍵在於AsyncOperation的syncContext字段,這是一個SynchronizationContext類型的對象,而這個對象的Post方法具體實現了封送,當我繼續查看
SynchronizationContext.Post方法時,裏面簡單的令人難以執行
public virtual void Post(SendOrPostCallback d, object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}
這是怎麼回事情呢,線程池本省並不具備線程封送的能力啊
聯想到在Winform程序和Console程序下程序的行爲是不同的,而且SynchronizationContext的Post方法是一個virtual方法,我猜測這個方法可能被繼承自他的類重寫了
查詢Msdn,果然發現在這個類有兩個子類,其中一個就是WindowsFormsSynchronizationContext,我們來看看這個類的Post方法
public override void Post(SendOrPostCallback d, object state)
{
if (this.controlToSendTo != null)
{
this.controlToSendTo.BeginInvoke(d, new object[] { state });
}
}
哈哈,又是熟悉的beginInvoke,原來控制檯程序和Winform程序加載的SynchronizationContext是不同的,所以行爲才有所不同,通過簡單的測試,我們可以看到控制檯程序直接使用基類(SynchronizationContext),而winform程序使用這個WindowsFormsSynchronizationContext的Post方法把方法調用封送到控件的線程.
總結:
同時這個類還提供了進度改變事件,允許用戶終止線程,功能全面,內部使用了線程池,能在一定成都上避免了大量線程的資源耗用問題,並通過SynchronizationContext解決了封送的問題,讓我們的回調事件代碼邏輯簡單清晰,推薦大家使用.
轉自:http://blog.csdn.net/songkexin/article/details/6178587