這個做法介紹了 BackgroundWorker
類別可以用來跨執行緒通訊。
BackgroundWorker
最簡單的用法就是建立一個委派並把它加入到 DoWork
事件中,然後呼叫 RunWorkerAsync
方法,可以看到
RunWorkerAsync
內部是透過 Task.Factory
來建立一個新的任務並執行 WorkerThreadStart
方法,最後就是 Invoke DoWorkEventHandler 開始執行設定的任務,
成功後會再次呼叫 OnRunWorkerCompleted
執行任務成功的相關操作,流程基本上就是以事件為主。
public event DoWorkEventHandler? DoWork;
public event RunWorkerCompletedEventHandler? RunWorkerCompleted;
public event ProgressChangedEventHandler? ProgressChanged;
protected virtual void OnDoWork(DoWorkEventArgs e)
{
DoWork?.Invoke(this, e);
}
public void RunWorkerAsync(object? argument)
{
if (_isRunning)
{
throw new InvalidOperationException(SR.BackgroundWorker_WorkerAlreadyRunning);
}
_isRunning = true;
_cancellationPending = false;
_asyncOperation = AsyncOperationManager.CreateOperation(null);
Task.Factory.StartNew(
WorkerThreadStart,
argument,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default
);
}
private void WorkerThreadStart(object? argument)
{
Debug.Assert(_asyncOperation != null, "_asyncOperation not initialized");
object? workerResult = null;
Exception? error = null;
bool cancelled = false;
try
{
DoWorkEventArgs doWorkArgs = new DoWorkEventArgs(argument);
OnDoWork(doWorkArgs);
if (doWorkArgs.Cancel)
{
cancelled = true;
}
else
{
workerResult = doWorkArgs.Result;
}
}
catch (Exception exception)
{
error = exception;
}
var e = new RunWorkerCompletedEventArgs(workerResult, error, cancelled);
_asyncOperation.PostOperationCompleted(_operationCompleted, e);
}
直接呼叫也很簡單,只需要加入委派之後呼叫 RunWorkerAsync
方法啟動即可。
void Main()
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
var bg = new BackgroundWorker();
bg.DoWork += ((sender, e) => {
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " In BackgroundWorker");
});
bg.RunWorkerAsync();
}
下面這個是一般 Event 的處理流程。
public event DoWorkEventHandler? DoWork;
void Main()
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
DoWork += ((sender, e) =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " In DoWorkEventHandler");
});
DoWork?.Invoke(this, null);
}
從這兩段程式碼可以明顯看出一般 Event 的處理流程是運行在主行緒上,但透過 BackgroundWorker
包裝過後的事件則是運行在新的執行緒上。
建立這個類別的目的就是重用背景執行的程式碼,並且方便使用者不用在寫前景執行緒與背景執行緒的溝通邏輯。
例如常用的進度回報、任務取消、異常處理等功能 BackgroundWorker
都有內建,所以我們就不用自己寫這方面的邏輯了。
下面這個例子就是啟用了回報功能與取消功能的 BackgroundWorker
,關鍵是在 DoWork
委派中呼叫 ReportProgress
方法,
回報目前的進度,並且加上 RunWorkerCompleted
事件添加任務完成後的處理邏輯,也可以添加判斷並呼叫 CancelAsync
,直接取消背景任務。
void Main()
{
var worker = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
worker.DoWork += (sender, e) => {
for (int i = 0; i < 100; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
return;
}
// throw new Exception();
Thread.Sleep(100);
(sender as BackgroundWorker)?.ReportProgress(i);
}
};
// 設置 ProgressChanged 事件
worker.ProgressChanged += (sender, e) =>
{
Console.WriteLine($"進度: {e.ProgressPercentage}%");
};
// 設置 RunWorkerCompleted 事件
worker.RunWorkerCompleted += (sender, e) =>
{
if (e.Cancelled)
Console.WriteLine("作業已取消");
else if (e.Error != null)
Console.WriteLine($"發生錯誤: {e.Error.Message}");
else
Console.WriteLine("作業完成");
};
worker.RunWorkerAsync();
// worker.CancelAsync();
}
另外一個重要的關鍵是錯誤處理,之前有提到過沒辦法把一個錯誤從 B 執行緒拋給 A 執行緒,這個問題在 BackgroundWorker
也有處理,
可以看一下 WorkerThreadStart
的原始碼它有透過 try/catch 捕捉錯誤但它沒有直接拋出,反而是透過 RunWorkerCompletedEventArgs
提供的 Error 屬性把錯誤資訊保存在裡面,最後需要在透過 RunWorkerCompletedEventArgs
事件參數讀取錯誤資訊進行後續處理。
Summary
這個做法介紹 BackgroundWorker
讓我們可以透過這個類別方便建立背景執行的工作,並且也提供了進度回報、任務取消、異常處理等常用功能,
避免每次都要自行實做前景與背景溝通的邏輯。