[轉載]使用BackgroundWorker元件進行非同步操作程式設計

weixin_34304013發表於2008-08-29

使用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的數值進行求和操作,介面如下,
CalculateAddForm.jpg
圖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;
}
  
上面的例子是在單個視窗中完成所有功能,可以對其進行簡單的修改實現在獨立對話方塊中顯示進度並提供取消操作的功能。
ProcessForm.jpg
圖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元件簡化了基於事件的非同步操作程式設計,根據其實現原理可進一步編寫支援多工的非同步操作元件來更好的滿足非同步操作密集的應用開發需求。

相關文章