概述
在應用程序中,可能會遇到一些執行耗時的功能操作,比如數據下載、復雜計算及數據庫事務等,一般這樣的功能會在單獨的線程上實現,執行結束後結果顯示到用戶界面上,這樣可避免造成用戶界面長時間無響應情況。在.NET 2.0及以後的版本中,FCL提供了BackgroundWorker組件來方便的實現這些功能要求。
組件介紹
BackgroundWorker 類位於System.ComponentModel 命名空間中,通過該類在單獨的線程上執行操作實現基於事件的異步模式。下面對BackgroundWorker類的主要成員進行介紹。
BackgroundWorker 類的第1個主要方法是RunWorkerAsync,該方法提交一個以異步方式啟動運行操作的請求,發出請求後,將引發 DoWork 事件,在事件處理程序中開始執行異步操作代碼。RunWorkerAsync 方法簽名如下,
public void RunWorkerAsync();
public void RunWorkerAsync(Object argument);
如果異步操作需要操作參數,可以將其作為argument參數提供,由於參數類型為Object,因此訪問時可能需要進行類型轉換。
CancelAsync 方法提交終止異步操作的請求,並將 CancellationPending 屬性設置為 true。需要注意的是,CancelAsync 方法是否調用成功,同WorkerSupportsCancellation 屬性相關,如果允許取消執行的異步操作,需將WorkerSupportsCancellation 屬性設置為true,否則調用該方法將拋出異常。CancelAsync方法不含參數,方法簽名如下,
public void CancelAsync();
調用 CancelAsync 方法時,BackgroundWorker的 CancellationPending 屬性值將被設置為true,因此在編寫單獨線程中執行的輔助方法時,代碼中應定期檢查CancellationPending 屬性,查看是否已將該屬性設置為 true,如果為true,應該結束輔助方法的執行。有一點需要注意的是,DoWork 事件處理程序中的代碼有可能在發出取消請求時已經完成處理工作,因此,DoWork事件處理程序或輔助方法可能會錯過設置 CancellationPending屬性為true的時機。在這種情況下,即使調用 CancelAsync方法發出了取消異步操作請求,RunWorkerCompleted 事件處理程序中RunWorkerCompletedEventArgs 參數的 Cancelled 標志也不會被設置為 true,這是在多線程編程中經常會出現的競爭條件問題,因此編寫代碼的時候需要考慮。
在執行異步操作時,如果需要跟蹤異步操作執行進度,BackgroundWorker類提供了 ReportProgress 方法,調用該方法將引發 ProgressChanged 事件,通過注冊該事件在事件處理程序中獲取異步執行進度信息。方法簽名如下:
public void ReportProgress(int percentProgress);
public void ReportProgress(int percentProgress,Object userState);
該方法包含兩個版本,percentProgress表示進度百分比,取值為0-100,userState為可選參數表示自定義用戶狀態。
同CancelAsync 方法一樣,BackgroundWorker的WorkerReportsProgress 屬性設置為 true時,ReportProgress 方法才會調用成功,否則將引發InvalidOperationException異常。
上面已經提到了BackgroundWorker的3個屬性,CancellationPending用來提示操作是否已經取消,WorkerReportsProgress和WorkerSupportsCancellation分別用來設置是否允許進度匯報和進行取消操作。
public bool CancellationPending { get; }
public bool WorkerReportsProgress { get; set; }
public bool WorkerSupportsCancellation { get; set; }
另外一個會用到的屬性是IsBusy,
public bool IsBusy { get; }
通過該屬性查詢BackgroundWorker實例是否正在運行異步操作,如果 BackgroundWorker 正在運行異步操作,則為true,否則為false。
BackgroundWorker 類包含3個事件,在事件處理程序中可進行異步操作輔助代碼編寫和同用戶界面信息交互。
public event DoWorkEventHandler DoWork;
public event ProgressChangedEventHandler ProgressChanged;
public event RunWorkerCompletedEventHandler 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類提供了對異步操作的生存期進行跟蹤的功能,包括操作進度通知和操作完成通知,並確保在正確的線程或上下文中調用客戶端的事件處理程序。
public void Post(SendOrPostCallback d,Object arg);
public void PostOperationCompleted(SendOrPostCallback d,Object arg);
通過在異步輔助代碼中調用Post方法把進度和中間結果報告給用戶,如果是取消異步任務或提示異步任務已完成,則通過調用 PostOperationCompleted方法結束異步操作的跟蹤生命期。在PostOperationCompleted方法調用後,AsyncOperation對象變得不再可用,再次訪問將引發異常。在兩個方法中都包含SendOrPostCallback委托參數,簽名如下,
public delegate void SendOrPostCallback(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 組件簡化了基於事件的異步操作編程,根據其實現原理可進一步編寫支持多任務的異步操作組件來更好的滿足異步操作密集的應用開發需求。