首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 委託異步編程全解析:從 BeginInvoke 到 async/await 的演進之路

     編輯:關於C語言

委託是 C# 異步編程的起點。本文層層遞進:多播委託返回值陷阱 → APM 模式 (BeginInvoke/EndInvoke) 三種調用方式 → 現代 Task/async-await 統一模型,並對比優劣。

多播委託的返回值陷阱

delegate int BinaryOp(int x, int y);

static int Add(int x, int y) => x + y;
static int Sub(int x, int y) => x - y;

static void Main() {
    BinaryOp op = Add;
    op += Sub; // 多播

    Console.WriteLine(op(3, 2)); // 輸出 1 (僅最後一個方法的返回值)
}

原因:CLR 依次調用鏈中每個委託,但只保留最後一次調用的返回值。

獲取所有返回值:

foreach (BinaryOp single in op.GetInvocationList()) {
    Console.WriteLine(single(3, 2)); // 依次輸出 5, 1
}

APM 模式 (Asynchronous Programming Model)

委託天生支持 BeginInvoke/EndInvoke,由線程池執行。

方式一:輪詢

BinaryOp op = Add;
IAsyncResult ar = op.BeginInvoke(3, 2, null, null);

while (!ar.IsCompleted) {
    Console.WriteLine("Working...");
    Thread.Sleep(100);
}
int result = op.EndInvoke(ar);
Console.WriteLine($"Result: {result}");

缺點:主線程忙等待,浪費 CPU。

方式二:等待句柄

IAsyncResult ar = op.BeginInvoke(3, 2, null, null);
ar.AsyncWaitHandle.WaitOne(); // 阻塞直到完成
int result = op.EndInvoke(ar);

改進:釋放線程,但依然同步阻塞。

方式三:回調 (最地道 APM)

BinaryOp op = Add;
op.BeginInvoke(3, 2, ar => {
    var result = ((BinaryOp)((AsyncResult)ar).AsyncDelegate).EndInvoke(ar);
    Console.WriteLine($"Callback: {result}, State: {ar.AsyncState}");
}, "UserData");

Console.WriteLine("Main thread free to do other work...");
Console.ReadLine();

關鍵點:

  • 回調在線程池線程執行
  • AsyncResult.AsyncDelegate 取得原委託實例
  • AsyncState 傳遞用戶數據

多播委託異步調用:每個目標獨立執行

BinaryOp op = Add;
op += Sub;

IAsyncResult ar = op.BeginInvoke(10, 5, null, null);
// 實際上會為鏈中每個委託分配獨立線程並行執行
// EndInvoke 僅返回最後一個完成的委託的結果
// 若需所有結果,必須拆分為單獨委託調用

APM 的痛點

  • 代碼分裂:邏輯拆散在發起與回調中
  • 異常處理困難:異常在 EndInvoke 拋出,需在回調中捕獲
  • 組合地獄:多個異步操作串並聯極其繁瑣
  • 無取消標準:需自行實現

現代解法:Task + async/await

.NET 4.0+ Task 統一了異步模型,C# 5.0 async/await 恢復同步寫法:

// 將 APM 封裝為 Task
public static Task AddAsync(int x, int y) {
    var tcs = new TaskCompletionSource();
    BinaryOp op = Add;
    op.BeginInvoke(x, y, ar => {
        try {
            tcs.SetResult(op.EndInvoke(ar));
        } catch (Exception ex) {
            tcs.SetException(ex);
        }
    }, null);
    return tcs.Task;
}

// 使用
public static async Task Demo() {
    int sum = await AddAsync(3, 2); // 非阻塞等待
    Console.WriteLine(sum);

    // 並行
    var tasks = new[] { AddAsync(1,2), AddAsync(3,4), AddAsync(5,6) };
    int[] results = await Task.WhenAll(tasks);

    // 超時取消
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
    try {
        await AddAsync(100, 200).WaitAsync(cts.Token);
    } catch (OperationCanceledException) {
        Console.WriteLine("Timed out");
    }
}

對比總結

維度Delegate APMTask + async/await
可讀性回調地獄同步風格,線性流程
異常處理分散在 EndInvoketry/catch 自然捕獲
組合手動協調WhenAll/WhenAny/ContinueWith
取消無標準CancellationToken 內建
進度報告自行實現IProgress<T> 標準化
調試堆棧破碎保留邏輯堆棟
性能較輕量狀態機開銷極小

遷移建議

  1. 新代碼直接用 async/await
  2. 舊 APM 代碼用 Task.Factory.FromAsync 或 TaskCompletionSource 封裝
  3. 委託異步僅用於極簡場景(如單一線程池任務,無組合需求)
  4. 注意:BeginInvoke 在 .NET Core / .NET 5+ 上不支持 (跨平台限制),必須遷移

結語

委託異步是 C# 異步編程的「遠古遺跡」,理解它能讓你更珍惜 Task/async-await 的優雅設計。現代 .NET 開發者應將 APM 視為歷史包袱,全面擁抱統一異步模型——除非你在維護 .NET Framework 2.0 時代的遺產系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved