使用BackgroundWorker元件進行異步操作程式設計
釋出日期:2008-06-18 | 更新日期:2008-06-18 作者:鄭佐 摘要:本文介紹了BackgroundWorker元件的功能及在基于事件的異步操作程式設計中的應用,并對元件的實作原理進行簡述。 下載下傳與本文相關的BackgroundWorkerSample示例代碼。 本頁内容 概述 元件介紹 應用示例 實作原理 結束語 概述 在應用程式中,可能會遇到一些執行耗時的功能操作,比如資料下載下傳、複雜計算及資料庫事務等,一般這樣的功能會在單獨的線程上實作,執行結束後結果顯示到使用者界面上,這樣可避免造成使用者界面長時間無響應情況。在.NET 2.0及以後的版本中,FCL提供了BackgroundWorker元件來友善的實作這些功能要求。 元件介紹 BackgroundWorker類位于System.ComponentModel 命名空間中,通過該類在單獨的線程上執行操作實作基于事件的異步模式。下面對BackgroundWorker類的主要成員進行介紹。 BackgroundWorker類的第1個主要方法是RunWorkerAsync,該方法送出一個以異步方式啟動運作操作的請求,送出請求後,将引發 DoWork 事件,在事件處理程式中開始執行異步操作代碼。RunWorkerAsync 方法簽名如下,
publicvoidRunWorkerAsync(); publicvoidRunWorkerAsync(Object argument); |
如果異步操作需要操作參數,可以将其作為argument參數提供,由于參數類型為Object,是以通路時可能需要進行類型轉換。 CancelAsync 方法送出終止異步操作的請求,并将 CancellationPending 屬性設定為 true。需要注意的是,CancelAsync 方法是否調用成功,同WorkerSupportsCancellation 屬性相關,如果允許取消執行的異步操作,需将WorkerSupportsCancellation 屬性設定為true,否則調用該方法将抛出異常。CancelAsync方法不含參數,方法簽名如下,
publicvoid CancelAsync(); |
調用 CancelAsync 方法時,BackgroundWorker的 CancellationPending 屬性值将被設定為true,是以在編寫單獨線程中執行的輔助方法時,代碼中應定期檢查 CancellationPending 屬性,檢視是否已将該屬性設定為 true,如果為true,應該結束輔助方法的執行。有一點需要注意的是,DoWork 事件處理程式中的代碼有可能在發出取消請求時已經完成處理工作,是以,DoWork事件處理程式或輔助方法可能會錯過設定CancellationPending屬性為true的時機。在這種情況下,即使調用 CancelAsync方法發出了取消異步操作請求,RunWorkerCompleted 事件處理程式中RunWorkerCompletedEventArgs 參數的 Cancelled 标志也不會被設定為 true,這是在多線程程式設計中經常會出現的競争條件問題,是以編寫代碼的時候需要考慮。 在執行異步操作時,如果需要跟蹤異步操作執行進度,BackgroundWorker類提供了 ReportProgress 方法,調用該方法将引發 ProgressChanged 事件,通過注冊該事件在事件處理程式中擷取異步執行進度資訊。方法簽名如下:
publicvoidReportProgress(int percentProgress); publicvoidReportProgress(int percentProgress,Object userState); |
該方法包含兩個版本,percentProgress表示進度百分比,取值為0-100,userState為可選參數表示自定義使用者狀态。 同CancelAsync 方法一樣,BackgroundWorker的WorkerReportsProgress 屬性設定為 true時,ReportProgress 方法才會調用成功,否則将引發InvalidOperationException異常。 上面已經提到了BackgroundWorker的3個屬性,CancellationPending用來提示操作是否已經取消,WorkerReportsProgress和WorkerSupportsCancellation分别用來設定是否允許進度彙報和進行取消操作。
publicboolCancellationPending { get; } publicboolWorkerReportsProgress { get; set; } publicboolWorkerSupportsCancellation { get; set; } |
另外一個會用到的屬性是IsBusy,
publicbool IsBusy { get; } |
通過該屬性查詢BackgroundWorker執行個體是否正在運作異步操作,如果 BackgroundWorker 正在運作異步操作,則為true,否則為false。 BackgroundWorker類包含3個事件,在事件處理程式中可進行異步操作輔助代碼編寫和同使用者界面資訊互動。
publiceventDoWorkEventHandler DoWork; publiceventProgressChangedEventHandler ProgressChanged; publiceventRunWorkerCompletedEventHandler RunWorkerCompleted; |
DoWork事件處理程式用來調用輔助方法進行實際處理操作,由于該事件處理程式在不同于UI的線程上執行,是以需要確定在 DoWork 事件處理程式中不操作任何使用者界面對象。如果輔助方法需要參數支援,可以通過RunWorkerAsync方法傳入,在 DoWork 事件處理程式中,通過 DoWorkEventArgs.Argument 屬性提取該參數。在異步操作期間,可以通過 ProgressChanged事件處理程式擷取異步操作進度資訊,通過RunWorkerCompleted 事件處理程式擷取異步操作結果資訊,在ProgressChanged和RunWorkerCompleted的事件處理程式中可以安全的同使用者界面進行通信。 應用示例 下面通過一個簡單的示例來示範BackgroundWorker元件的典型應用。在本示例中,實作一個數值的求和操作,該操作本身運作很快,為模拟處理過程有一個可感覺的時間段,在輔助方法中調用了Thread.Sleep方法。 示例程式通過Windows Forms展示,顯示了對1-100的數值進行求和操作,界面如下,
圖1:應用程式界面 下面對主要實作代碼進行說明,先看一下BackgroundWorker類的初始化,在初始化過程中注冊了3個事件,允許異步輔助方法調用,以及異步操作進度通知和操作取消。
private System.ComponentModel.BackgroundWorker backgroundWorker1; private void InitializeBackgoundWorker() { this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); this.backgroundWorker1.WorkerReportsProgress = true; this.backgroundWorker1.WorkerSupportsCancellation = true; this.backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); } |
通過StartAsync按鈕事件處理程式開始異步處理操作請求,事件處理程式如下,
private void startAsyncButton_Click(object sender, EventArgs e) { resultLabel.Text = String.Empty; this.numericUpDown1.Enabled = false; this.startAsyncButton.Enabled = false; this.cancelAsyncButton.Enabled = true; //擷取計算數值. int numberToCompute = (int)numericUpDown1.Value; //啟動異步操作. backgroundWorker1.RunWorkerAsync(numberToCompute); } |
startAsyncButton_Click處理程式首先對一些界面控件進行狀态設定,然後調用BackgroundWorker執行個體的RunWorkerAsync方法開始執行異步操作,而此時就會觸發DoWork事件。
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; e.Result = ComputeAdd((int)e.Argument, worker, e); } |
在DoWork事件處理程式中,通過DoWorkEventArgs.Argument屬性擷取傳入的參數傳遞給ComputeAdd輔助方法,并把處理結果儲存到DoWorkEventArgs.Result屬性中,最後在RunWorkerCompleted 事件處理程式的RunWorkerCompletedEventArgs.Result 屬性中擷取處理結果。如果在DoWork事件處理程式中出現異常,則 BackgroundWorker 将捕獲該異常并将其傳遞到 RunWorkerCompleted 事件處理程式,在該事件處理程式中,異常資訊作為 RunWorkerCompletedEventArgs 的 Error 屬性公開。
private long ComputeAdd(int n, BackgroundWorker worker, DoWorkEventArgs e) { long result = 0; for (int i = 1; i <= n; i++) { if (worker.CancellationPending) { e.Cancel = true; break; } else { result += i; Thread.Sleep(500); int percentComplete = (int)((float)i / (float)n * 100); worker.ReportProgress(percentComplete); } } return result; } |
在輔助方法中,代碼定期通路BackgroundWorker執行個體的CancellationPending屬性,如果調用了BackgroundWorker的CancelAsync 方法,那麼CancellationPending屬性值就會被設定為true,輔助方法就結束執行。另外,在輔助方法中實作了進度彙報功能,通過調用worker.ReportProgress方法觸發ProgressChanged事件,接着通過ProgressChanged事件處理程式來更新進度顯示。
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar1.Value = e.ProgressPercentage; } |
最後,在RunWorkerCompleted事件處理程式中可以得到異步處理結果資訊,分析異步操作是正常執行結束還是在進行中被取消或者是執行出現錯誤異常而終止。對于處理結果資訊的通路有一個标準的順序,先是判斷異步處理是否異常結束,接着判斷是否執行了取消操作,最後通路處理結果。
void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show(e.Error.Message); } else if (e.Cancelled) { resultLabel.Text = "Canceled"; } else { resultLabel.Text = e.Result.ToString(); } this.numericUpDown1.Enabled = true; startAsyncButton.Enabled = true; cancelAsyncButton.Enabled = false; } |
上面的例子是在單個視窗中完成所有功能,可以對其進行簡單的修改實作在獨立對話框中顯示進度并提供取消操作的功能。
圖2:進度顯示對話框 建立一個窗體命名為ProcessForm用來顯示異步操作進度,對ProcessForm類的預設構造函數進行修改,傳入BackgroundWorker執行個體的引用,注冊ProgressChanged事件實作窗體進度條的更新,注冊RunWorkerCompleted事件通知ProcessForm窗體關閉。
public ProcessForm(BackgroundWorker backgroundWorker1) { InitializeComponent(); this.backgroundWorker1 = backgroundWorker1; this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); } void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.Close(); } void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar1.Value = e.ProgressPercentage; } private void cancelButton1_Click(object sender, EventArgs e) { this.backgroundWorker1.CancelAsync(); this.cancelButton1.Enabled = false; this.Close(); } |
對于進度視窗的顯示方式可以是模式視窗或非模式視窗,兩者的實作代碼并沒有太大差別,改進後的StartAsync按鈕事件處理程式如下。
private void startAsyncButton_Click(object sender, EventArgs e) { // ... backgroundWorker1.RunWorkerAsync(numberToCompute); ProcessForm form = new ProcessForm(this.backgroundWorker1); form.ShowDialog(this);//模式 //form.Show(this);//非模式 } |
實作原理 在分析BackgroundWorker實作原理之前,需要了解一下在.NET Framework 2.0版本中新增加的兩個類。AsyncOperationManager 類和AsyncOperation 類都位于System.ComponentModel 命名空間中,AsyncOperation類提供了對異步操作的生存期進行跟蹤的功能,包括操作進度通知和操作完成通知,并確定在正确的線程或上下文中調用用戶端的事件處理程式。
publicvoidPost(SendOrPostCallback d,Object arg); publicvoidPostOperationCompleted(SendOrPostCallback d,Object arg); |
通過在異步輔助代碼中調用Post方法把進度和中間結果報告給使用者,如果是取消異步任務或提示異步任務已完成,則通過調用PostOperationCompleted方法結束異步操作的跟蹤生命期。在PostOperationCompleted方法調用後,AsyncOperation對象變得不再可用,再次通路将引發異常。在兩個方法中都包含SendOrPostCallback委托參數,簽名如下,
publicdelegatevoidSendOrPostCallback(Object state); |
SendOrPostCallback 委托用來表示在消息即将被排程到同步上下文時要執行的回調方法。 AsyncOperation類看上去很強大,不過有開發人員反映該類的.NET 2.0版本存在Bug,在3.0及後面的版本微軟是否進行過更新還需進一步考證。筆者在控制台應用程式中進行測試,asyncOperation的Post方法遞交的SendOrPostCallback委托不一定是在控制台主線程執行,通過通路System.Threading.Thread.CurrentThread.ManagedThreadId可以确認這一點,奇怪的是控制台程式未發現運作異常,這個可能是控制台程式執行方式不同于窗體程式的原因。 AsyncOperationManager 類為AsyncOperation對象的建立提供了便捷方式,通過CreateOperation方法可以建立多個AsyncOperation執行個體,實作對多個異步操作進行跟蹤。 BackgroundWorker元件通過DoWork事件實作了在單獨的線程上執行操作,其内部通過異步委托來完成,在BackgroundWorker類内部聲明了WorkerThreadStartDelegate委托,并定義了threadStart成員變量,同時在構造函數中初始化threadStart。
private delegate void WorkerThreadStartDelegate(object argument); private readonly WorkerThreadStartDelegate threadStart; public BackgroundWorker() { this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); //… } |
BackgroundWorker通過調用RunWorkerAsync方法開始執行異步操作請求,并在方法體中調用threadStart.BeginInvoke方法實作異步調用。
public void RunWorkerAsync(object argument) { if (this.isRunning) { throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning")); } this.isRunning = true; this.cancellationPending = false; this.asyncOperation = AsyncOperationManager.CreateOperation(null); this.threadStart.BeginInvoke(argument, null, null); } |
在threadStart委托中指定的WorkerThreadStart方法将觸發DoWork事件,使用者通過注冊DoWork事件執行異步代碼的操作,從下面的代碼可以看出在DoWork事件處理程式中不能通路UI元素的原因。
private void WorkerThreadStart(object argument) { object result = null; Exception error = null; bool cancelled = false; try { DoWorkEventArgs e = new DoWorkEventArgs(argument); this.OnDoWork(e); if (e.Cancel) { cancelled = true; } else { result = e.Result; } } catch (Exception exception2) { error = exception2; } RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(result, error, cancelled); this.asyncOperation.PostOperationCompleted(this.operationCompleted, arg); } |
在上述代碼中,this.OnDoWork(e)方法産生DoWork事件,DoWork事件處理程式執行完成後會判斷在事件處理程式中是否對DoWorkEventArgs.Cancel屬性進行了設定,如果使用者調用了CancelAsync 方法那麼DoWorkEventArgs.Cancel會被設定為true,事件處理程式正常執行完成時可以從DoWorkEventArgs.Result得到執行結果,如果出現處理異常将撲獲異常,所有需要的資訊将包含在RunWorkerCompletedEventArgs執行個體中,最後執行asyncOperation.PostOperationCompleted方法産生RunWorkerCompleted 事件,是以在RunWorkerCompleted事件處理程式中可以獲得取消操作、處理異常或處理結果的資訊。 類似于RunWorkerCompleted事件的發生機制,對于異步操作進度通知事件發生通過ReportProgress方法實作。
public void ReportProgress(int percentProgress, object userState) { if (!this.WorkerReportsProgress) { throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntReportProgress")); } ProgressChangedEventArgs arg = new ProgressChangedEventArgs(percentProgress, userState); if (this.asyncOperation != null) { this.asyncOperation.Post(this.progressReporter, arg); } else { this.progressReporter(arg); } } |
調用者在DoWork事件處理程式中通過調用ReportProgress方法進行進度彙報,其内部通過asyncOperation.Post方法産生ProgressChanged 事件,如果asyncOperation為null,那麼就調用progressReporter方法産生事件,但是調用progressReporter方法産生事件明顯存在問題,因為這樣産生的事件所線上程同DoWork事件為同一線程,ProgressChanged事件處理程式也會執行在DoWork線程同一上下文中,是以在ProgressChanged事件處理程式中通路ProgressBar控件将出現“線程間操作無效: 從不是建立控件“progressBar1”的線程通路它。”的異常。筆者認為這樣的處理是元件的一個Bug,如果asyncOperation為null,更好的處理方式是抛出異常或不做通知處理。值得一提的是,在控制台應用程式中測試調用progressReporter方法不會出現“線程間操作無效”的異常。 結合構造函數,下面的代碼有助于進一步了解ProgressChanged事件和RunWorkerCompleted事件産生機制。
public BackgroundWorker() { this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted); this.progressReporter = new SendOrPostCallback(this.ProgressReporter); } private void ProgressReporter(object arg) { this.OnProgressChanged((ProgressChangedEventArgs)arg); } private void AsyncOperationCompleted(object arg) { this.isRunning = false; this.cancellationPending = false; this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs)arg); } |
最後,看一下RunWorkerAsync方法和CancelAsync方法的實作。
public void RunWorkerAsync(object argument) { if (this.isRunning) { throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning")); } this.isRunning = true; this.cancellationPending = false; this.asyncOperation = AsyncOperationManager.CreateOperation(null); this.threadStart.BeginInvoke(argument, null, null); } public void CancelAsync() { if (!this.WorkerSupportsCancellation) { throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntSupportCancellation")); } this.cancellationPending = true; } |
結束語 BackgroundWorker元件簡化了基于事件的異步操作程式設計,根據其實作原理可進一步編寫支援多任務的異步操作元件來更好的滿足異步操作密集的應用開發需求。