在 .NET Framework 4.0 推出 Task 與平行運算庫(TPL)之前,開發者若身處 .NET 3.5 專案環境,要實現非同步操作往往只能仰賴傳統的 BeginInvoke/EndInvoke 模式(APM)。本文將示範如何封裝一套輕量級的非同步程式框架,提供類似 Task.ContinueWith 的鏈式呼叫、例外處理、集合平行處理等功能,讓舊專案也能享受現代化的非同步開發體驗。
框架由四大核心類別組成,分工明確:
Action 委派。Func<TResult> 委派。Invoke、WaitAll、Foreach、ContinueWithAsync 等便利方法,對外隱藏細節。public interface IOperationAsync
{
IAsyncResult Invoke(); // 啟動非同步作業
void Wait(); // 同步等待完成
void CompletedCallBack(IAsyncResult ar); // 完成回呼
void CatchException(Exception ex); // 例外捕捉
}
public interface IContinueWithAsync
{
Operator Previous { get; set; } // 上一個作業
Operator Next { get; set; } // 下一個作業
Operator ContinueWithAsync(Action action);
Operator ContinueWithAsync<TParameter>(Action<TParameter> action, TParameter parameter);
Operator ContinueWithAsync<TResult>(Func<TResult> func);
Operator ContinueWithAsync<TParameter, TResult>(Func<TParameter, TResult> func, TParameter parameter);
}
Operator 同時實作上述兩個介面,內部持有 IAsyncResult Middle 作為目前作業的狀態控制代碼。關鍵邏輯如下:
Wait():若作業未完成,呼叫 AsyncWaitHandle.WaitOne() 封鎖執行緒。ContinueWithAsync 系列方法:建立下一個 Operator(ActionAsync 或 FuncAsync),設定 Previous/Next 雙向連結,並回傳新節點以支援鏈式語法。ContinueAsync():在回呼完成時自動啟動 Next.Invoke(),實現自動延續。兩者皆繼承 Operator,差別在於委派型別與結果處理:
_action.BeginInvoke(CompletedCallBack, null) 啟動,回呼中呼叫 EndInvoke 並觸發 ContinueAsync()。IFuncOperationAsync<TResult>,回呼中以 _func1.EndInvoke(ar) 取得回傳值,透過 SetResult 保存,GetResult() 內部會先 Wait() 確保完成再回傳。| 方法 | 用途 |
|---|---|
Invoke(Action) | 啟動無參數無回傳非同步作業 |
Invoke<T>(Action<T>, T) | 啟動帶單一參數的 Action |
Invoke<TResult>(Func<TResult>) | 啟動無參數有回傳非同步作業 |
WaitAll(IEnumerable<Operator>) | 阻塞直到所有作業完成 |
WaitAny(IEnumerable<Operator>) | 輪詢等待任一作業完成 |
Foreach<T>(IEnumerable<T>, Action<T>) | 對集合每個元素平行啟動 Action,回傳 Operator 集合 |
ContinueWithAsync(operators, action) | 在指定作業群全部完成後延續執行 |
Asynchronous.Invoke(() => Console.WriteLine("背景作業執行中..."));
var op = Asynchronous.Invoke(() => ComputeHeavy());
op.Wait();
Console.WriteLine($"結果:{((FuncAsync<int>)op).Result}");
int[] data = {1,2,3,4,5};
var ops = Asynchronous.Foreach(data, x => x * 2); // 平行啟動
Asynchronous.WaitAll(ops); // 等全部完成
Asynchronous.Foreach(ops.Cast<FuncAsync<int,int>>(),
o => Console.WriteLine(o.Result));
Asynchronous.Invoke(Console.WriteLine, 1)
.ContinueWithAsync(Console.WriteLine, 2)
.ContinueWithAsync(Console.WriteLine, 3);
// 依序輸出 1, 2, 3
var ops = Asynchronous.Foreach(data, x => x * 2);
Asynchronous.Foreach(ops, o =>
o.ContinueWithAsync(() =>
{
if (o.Exception != null)
LogError(o.Exception);
else
Console.WriteLine($"項目完成:{((FuncAsync<int,int>)o).Result}");
}));
ContinueWithAsync 回傳的 Operator 若直接呼叫 Wait(),僅會等待該節點本身,不會自動往前追溯;建議將所有需等待的作業收集後統一 WaitAll。SemaphoreSlim 限制平行度。Operator.Exception 屬性,不會自動向上拋出,務必在延續或最後統一檢查。這套框架雖無法取代 .NET 4.0+ 的 Task/async-await,但在受限於 .NET 3.5 的存續專案中,能以極低成本引入現代化的非同步模式:啟動、等待、延續、集合平行、例外集中處理一應俱全。若團隊規劃遷移至新版框架,程式碼結構也極易對應至 Task.Run、Task.WhenAll、ContinueWith,遷移成本極低。