概述
在應用程序中,可能會遇到一些執行耗時的功能操作,比如數據下載、復雜計算及數據庫事務等,一般這樣的功能會在單獨的線程上實現,避免出現用戶界面長時間無響應情況。在.NET 2.0中,FCL提供了BackgroundWorker組件來方便的實現這些功能要求,該組件在功能上的確很吸引人。本文將實現一個增強的BackgroundWorker組件,支持基於事件的多任務異步操作。
功能介紹
BackgroundWorker 組件采用基於事件的異步模式簡化了多線程操作編程,不過其不能對多個異步任務生命期進行管理,因此開發人員可能會通過使用多個 BackgroundWorker實例來應對異步操作密集的情況。MultiBackgroundWorker組件解決多任務的問題,使得單個實例對多個異步任務的生命期進行集中管理,對於每個任務同樣提供異步任務操作請求,異步任務執行進度匯報以及異步任務結束通知。
異步任務操作請求。MultiBackgroundWorker組件提供了RunWorkerAsync方法來開始一個異步操作的請求,該方法需要一個參數來唯一標識新的異步任務,如果任務執行過程中需要用到相關信息數據可以通過第二個參數傳入。
public virtualvoid RunWorkerAsync(object taskId, object argument);
在RunWorkerAsync方法調用後,MultiBackgroundWorker會生成一個新的異步任務,並對其生命周期進行管理,同時觸發DoWork 事件。
public event MultiDoWorkEventHandler DoWork;
在事件處理程序中通過MultiDoWorkEventArgs對象獲取參數信息和任務標識。類似於BackgroundWorker 組件,調用程序注冊DoWork 事件並在該事件處理程序中編寫異步處理邏輯代碼。DoWork事件處理程序執行的線程不同於調用RunWorkerAsync 方法的線程,因此,調用線程是UI線程時,在DoWork 事件處理程序中就不能編寫訪問UI元素的代碼,而實際在編寫WinForm應用程序時調用線程基本上UI線程,值得注意。
異步任務執行進度匯報。同BackgroundWorker組件一樣,MultiBackgroundWorker同樣提供了ReportProgress方法。
public void ReportProgress(object taskId, int progressPercentage, object userState);
方法的第一個參數為進度匯報對應的任務標識, 第二個參數為已完成的異步任務操作進度所占的百分比,取值范圍從0到100,最後一個參數為用戶自定義信息。在調用ReportProgress 方法後將激發ProgressChanged事件。
public event MultiProgressChangedEventHandler ProgressChanged;
調用程序在對應的事件處理程序中可以訪問MultiProgressChangedEventArgs對象獲取進度信息並顯示到界面上。
異步任務結束通知。如果調用程序需要獲得每個異步任務結束通知以便執行一些後續代碼,那麼可以通過注冊RunWorkerCompleted 事件來實現。
public event MultiRunWorkerCompletedEventHandler RunWorkerCompleted;
異步任務人為取消、異常終止或正常結束都會觸發RunWorkerCompleted事件,並且在該事件處理程序中,通過 MultiRunWorkerCompletedEventArgs對象獲取處理結果信息。在事件處理程序中,一般遵循“ 先判斷異步任務是否異常結束,接著判斷是否執行了取消操作,最後訪問處理結果 ”的步驟。
MultiBackgroundWorker 組件提供了另外兩個方法以便更好的工作。CancelAsync方法用來取消異步任務的執行,該方法需要提供任務標識以便讓組件知道需要對哪個任務執行取消操作。TaskCanceled 方法判斷異步任務是否已經取消。
public void CancelAsync(object taskId);
public bool TaskCanceled(object taskId);
應用示例
本文通過一個簡單的WinForm程序來演示MultiBackgroundWorker組件如何簡化多任務異步模式編程。
MultiBackgroundWorkerSample 應用程序實現了多任務求和計算功能,用戶可以在界面上添加任務,取消任務和刪除任務,對於正在執行的任務可以查看其處理進程以及最後處理結果。為更好的演示多任務異步模式,程序界面顯示了主線程Id和各個任務線程Id。
圖1:應用程序界面
上圖展示了程序主界面,窗體上包含4個控件。ListView顯示了任務的執行情況,針對每項任務顯示任務Id、求和計算數值、處理進程、任務線程Id以及處理結果。三個按鈕控件用來處理任務開始、任務取消和任務移除。
MultiBackgroundWorker 組件在MainForm類的構造函數中被實例化,並注冊相關事件。
multiBackgroundWorker1 = new MultiBackgroundWorker();
multiBackgroundWorker1.DoWork +=
new MultiDoWorkEventHandler(multiBackgroundWorker1_DoWork);
multiBackgroundWorker1.ProgressChanged +=
new MultiProgressChangedEventHandler(multiBackgroundWorker1_ProgressChanged);
multiBackgroundWorker1.RunWorkerCompleted +=
new MultiRunWorkerCompletedEventHandler(multiBackgroundWorker1_RunWorkerCompleted);
新任務通過Start按鈕創建,在按鈕事件處理程序中,程序隨機生成一個50到100之間的數值,作為求和計算的最大加數。新的任務通過生成Guid字符串來表示其唯一性,在任務初始化後作為ListViewItem項添加到ListView進行顯示,最後 MultiBackgroundWorker組件執行RunWorkerAsync方法啟動異步操作請求。
private void startButton_Click(object sender, EventArgs e)
{
Random rand = new Random();
int testNumber = rand.Next(50,100);
string taskId = Guid.NewGuid().ToString();
this.AddListViewItem(taskId, testNumber);
this.multiBackgroundWorker1.RunWorkerAsync(taskId, testNumber);
}
在組件的RunWorkerAsync方法調用後,DoWork 事件激發,在事件處理程序中具體實現了異步操作任務,可以注意到從DoWork事件處理程序到輔助方法代碼都沒有對UI元素進行訪問。
void multiBackgroundWorker1_DoWork(object sender, MultiDoWorkEventArgs e)
{
int n = (int)e.Argument;
object taskId = e.TaskId;
e.Result = ComputeAdd(n, taskId, (MultiBackgroundWorker)sender, e);
}
MultiDoWorkEventArgs 參數包含了調用程序關心的信息,程序通過ComputeAdd輔助方法進行實際的求和計算,由於該運算過程很快,因此通過Thread.Sleep方法來降低其計算速度。在計算的循環體中,通過worker.TaskCanceled方法判斷當前任務是否發出了取消請求,如果該任務已經被取消那麼退出計算過程。
private long ComputeAdd(int n, object taskId, MultiBackgroundWorker worker, MultiDoWorkEventArgs e)
{
long result = 0;
for (int i = 1; i <= n; i++)
{
if (worker.TaskCanceled(taskId))
{
e.Cancel = true;
break;
}
result += i;
Thread.Sleep(1000);
int progressPercentage = (int)((float)i / (float)n * 100);
worker.ReportProgress(taskId, progressPercentage,
Thread.CurrentThread.ManagedThreadId);
}
return result;
}
在輔助方法代碼中,程序在循環體內執行了worker.ReportProgress方法產生ProgressChanged事件來向調用程序公開訪問接口,調用程序通過MultiProgressChangedEventArgs對象獲取任務進度信息並更新信息到ListView進行顯示,UserState屬性包含了托管線程Id信息。
void multiBackgroundWorker1_ProgressChanged(object sender, MultiProgressChangedEventArgs e)
{
UpdateListViewItem((string)e.TaskId, e.ProgressPercentage, (int)e.UserState);
}
在每一個任務執行完成、出現異常或被取消後MultiBackgroundWorker 組件都會激發RunWorkerCompleted 事件,調用程序訪問對應的事件處理程序獲得處理結果。調用程序先判斷異步任務是否異常結束,然後判斷是否執行了取消操作,最後訪問處理結果,並顯示結果到ListView。
void multiBackgroundWorker1_RunWorkerCompleted(object sender, MultiRunWorkerCompletedEventArgs e)
{
string taskId = (string)e.TaskId;
ListViewItem item;
if (e.Error != null)
{
item = UpdateListViewItem(taskId, "Error");
}
else if (e.Cancelled)
{
item = UpdateListViewItem(taskId, "Canceled");
}
else
{
string result = ((long)e.Result).ToString(CultureInfo.CurrentCulture.NumberFormat);
item = UpdateListViewItem(taskId, result);
}
item.Tag = null;//設置TaskId為空。
}
另外,程序實現了對正在執行的任務進行取消的功能,通過組件的CancelAsync 方法發出取消請求。
private void cancelButton_Click(object sender, EventArgs e)
{
foreach (ListViewItem lvi in this.listView1.SelectedItems)
{
if (lvi.Tag != null)
{
string taskId = (string)lvi.Tag;
this.multiBackgroundWorker1.CancelAsync(taskId);
lvi.Selected = false;
}
}
cancelButton.Enabled = false;
}
實現原理
MultiBackgroundWorker 組件的實現原理同BackgroundWorker相同,使用基於事件的異步模式實現多線程編程。
MultiBackgroundWorker 組件部分功能的實現使用了.NET 2.0中新增的類型,因此先來了解一下這些新類型。在.NET Framework 2.0版本中FCL在System.ComponentModel 命名空間下增加了AsyncOperationManager 類和AsyncOperation 類來輔助異步操作編程。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 委托用來表示在消息即將被調度到同步上下文時要執行的回調方法。AsyncOperationManager類為AsyncOperation對象的創建提供了便捷方式,通過CreateOperation方法可以創建多個AsyncOperation實例,實現對多個異步操作進行跟蹤。
那麼MultiBackgroundWorker組件如何實現對多個異步任務的生命周期進行管理?在MultiBackgroundWorker類內部定義了一個HybridDictionary容器類來保存各個任務的引用,在有新任務產生時容器會增加一項針對新任務的引用,在任務處理結束後容器會移除該任務的引用信息。由於容器需要被多個線程訪問,因此必須是線程安全的。
private HybridDictionary userStateToLifetime = new HybridDictionary();
組件通過RunWorkerAsync 方法為每個任務創建對應的AsyncOperation實例保存到任務容器中,並通過異步委托啟動異步操作。
public virtual void RunWorkerAsync(object taskId, object argument)
{
AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(taskId);
lock (userStateToLifetime.SyncRoot)
{
if (userStateToLifetime.Contains(taskId))
{
throw new ArgumentException("Task ID parameter must be unique");
}
userStateToLifetime[taskId] = asyncOp;
}
threadStart.BeginInvoke(taskId, argument, null, null);
}
MultiBackgroundWorker 類內部定義了WorkerThreadStartDelegate委托,threadStart 是該委托變量。
private delegatevoid WorkerThreadStartDelegate(object taskId, object argument);
private readonlyWorkerThreadStartDelegate threadStart;
在構造函數中,該委托變量通過指定WorkerThreadStart 方法 被實例化 。
this.threadStart = newWorkerThreadStartDelegate(this.WorkerThreadStart);
WorkerThreadStart 方法可以說是整個組件的核心,該方法在內部通過調用OnDoWork方法觸發DoWork事件,在DoWork事件處理程序執行完成後,通過檢查 MultiDoWorkEventArgs對象Cancel屬性來判斷用戶是否進行取消操作的請求;如果Cancel屬性為False,那麼獲取處理結果;如果DoWork事件處理程序執行過程中出現異常,那麼撲獲異常。在該方法中,任務對應的AsyncOperation對象被從任務容器中移除,標志著該任務生命周期的結束。方法體的最後調用AsyncOperation 的PostOperationCompleted方法觸發RunWorkerCompleted事件,可以看到 MultiRunWorkerCompletedEventArgs對象包含了任務ID、任務執行結果、錯誤信息和取消標志。
private void WorkerThreadStart(object taskId, object argument)
{
object result = null;
Exception error = null;
bool cancelled = false;
AsyncOperation asyncOp = GetAsyncOperation(taskId);
try
{
MultiDoWorkEventArgs e = new MultiDoWorkEventArgs(taskId, argument);
this.OnDoWork(e);
if (e.Cancel)//需要標示完成還是退出
{
cancelled = true;
}
else
{
result = e.Result;
}
}
catch (Exception ex)
{
error = ex;
}
if (TaskCanceled(taskId) == false)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
MultiRunWorkerCompletedEventArgs ee =
new MultiRunWorkerCompletedEventArgs(taskId, result, error, cancelled);
asyncOp.PostOperationCompleted(onAsyncOperationCompletedDelegate, ee);
}
對於異步任務進度匯報功能的實現,組件的ReportProgress 方法在內部調用AsyncOperation的Post方法觸發ProgressChanged事件來完成。
public void ReportProgress(object taskId, int progressPercentage, object userState)
{
AsyncOperation asyncOp = GetAsyncOperation(taskId);
if (asyncOp != null)
{
ProgressChangedEventArgs e =
new MultiProgressChangedEventArgs(taskId, progressPercentage, userState);
asyncOp.Post(this.onProgressReportDelegate, e);
}
}
onProgressReportDelegate 為SendOrPostCallback委托對象,在構造函數中進行初始化,
this.onProgressReportDelegate = new SendOrPostCallback(ProgressReport);
通過下面的兩個輔助方法最後產生ProgressChanged 事件。
private void ProgressReport(object state)
{
MultiProgressChangedEventArgs e = state as MultiProgressChangedEventArgs;
OnProgressChanged(e);
}
protected virtual void OnProgressChanged(MultiProgressChangedEventArgs e)
{
if (ProgressChanged != null)
{
ProgressChanged(this, e);
}
}
DoWork 事件處理程序的線程是異步任務線程,而ProgressChanged 事件處理程序的線程是實例化MultiBackgroundWorker組件的線程,AsyncOperation通過Post方法神奇的解決了該問題。
同樣,RunWorkerCompleted事件也通過下面的兩個輔助方法得到觸發,線程問題通過AsyncOperation的PostOperationCompleted方法解決。
private void MultiRunWorkerCompleted(object operationState)
{
MultiRunWorkerCompletedEventArgs e =
operationState as MultiRunWorkerCompletedEventArgs;
OnRunWorkerCompleted(e);
}
protected virtual void OnRunWorkerCompleted(MultiRunWorkerCompletedEventArgs e)
{
if (RunWorkerCompleted != null)
{
RunWorkerCompleted(this, e);
}
}
最後我們來看一下任務狀態查詢和任務取消方法的實現。對於任務狀態的查詢,只需根據TaskId查詢一下內部容器,如果容器中沒有包含任務對應的AsyncOperation 對象說明任務已經結束或者取消,否則認為任務還在執行中;同樣,要取消一個異步任務,只需要根據TaskId判斷容器是否存在對應的 AsyncOperation對象,如果存在就從容器中移除,從而實現任務取消請求。
public bool TaskCanceled(object taskId)
{
return (userStateToLifetime[taskId] == null);
}
public void CancelAsync(object taskId)
{
AsyncOperation asyncOp = userStateToLifetime[taskId] as AsyncOperation;
if (asyncOp != null)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
}
結束語
MultiBackgroundWorker 組件簡化了基於事件的多任務異步操作編程,由於功能比較清晰,作者沒有為組件實現設計時支持,因為不是很需要。