在上一篇文章中,我們初步認識了 .NET 透過委派實現非同步呼叫的基本概念。本文將更進一步,透過擴充 AsyncTest 類別、引入新的委派型別,並搭配 .NET Reflector 反編譯分析,深入探討 BeginInvoke / EndInvoke 這對核心方法的內部機制、參數差異、回呼模式與異常處理,最後示範如何為自訂類別新增符合 .NET 設計規範的非同步介面。
我們在 AsyncTest 中加入 YearlySalary 方法,並為其定義對應的委派 SalaryEventHandler:
public delegate string AsyncEventHandler(string name); // 對應 Hello 方法
public delegate decimal SalaryEventHandler(decimal salary, int monthCount, decimal bonus); // 對應 YearlySalary 方法
public class AsyncTest
{
public string Hello(string name)
{
return "Hello:" + name;
}
///
/// 計算一年的薪水
///
public decimal YearlySalary(decimal salary, int monthCount, decimal bonus)
{
Console.WriteLine("Thread ID:#{0}", Thread.CurrentThread.ManagedThreadId);
return salary * monthCount + bonus;
}
}
使用 .NET Reflector 觀察編譯器為兩個委派產生的類別結構:
public sealed class SalaryEventHandler : MulticastDelegate
{
public SalaryEventHandler(object @object, IntPtr method) { }
public virtual IAsyncResult BeginInvoke(decimal salary, int monthCount, decimal bonus, AsyncCallback callback, object @object) { }
public virtual decimal EndInvoke(IAsyncResult result) { }
public virtual decimal Invoke(decimal salary, int monthCount, decimal bonus) { }
}
public sealed class AsyncEventHandler : MulticastDelegate
{
public AsyncEventHandler(object @object, IntPtr method) { }
public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object) { }
public virtual string EndInvoke(IAsyncResult result) { }
public virtual string Invoke(string name) { }
}
關鍵觀察: 兩者皆繼承 MulticastDelegate,並擁有三個 virtual 方法:Invoke(同步)、BeginInvoke / EndInvoke(非同步成對)。參數差異在於前者依委派簽名動態生成,但後兩個參數 AsyncCallback 與 object state 永遠固定。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main Thread ID:#{0}", Thread.CurrentThread.ManagedThreadId);
AsyncTest test = new AsyncTest();
SalaryEventHandler salaryDelegate = test.YearlySalary;
// [1] 直接呼叫
decimal v1 = test.YearlySalary(100000, 15, 100000);
// [2] 委派直接叫用(編譯器自動轉為 Invoke)
decimal v2 = salaryDelegate(100000, 15, 100000);
// [3] 顯式呼叫 Invoke
decimal v3 = salaryDelegate.Invoke(100000, 15, 100000);
Console.WriteLine("V1:{0}, V2:{1}, V3:{2}", v1, v2, v3);
Console.ReadLine();
}
}
輸出顯示三者皆在同一執行緒(例如 #10),證明均為同步阻塞呼叫。委派此處扮演「代理人」角色,參數與回傳值必須與原方法一致。
IAsyncResult 物件(儲存非同步操作狀態)。| 委派 | BeginInvoke 簽名 | EndInvoke 簽名 |
|---|---|---|
| SalaryEventHandler | IAsyncResult BeginInvoke(decimal, int, decimal, AsyncCallback, object) | decimal EndInvoke(IAsyncResult) |
| AsyncEventHandler | IAsyncResult BeginInvoke(string, AsyncCallback, object) | string EndInvoke(IAsyncResult) |
共同點:回傳 IAsyncResult、最後兩參數固定為 AsyncCallback 與 object state。EndInvoke 參數皆為 IAsyncResult,回傳型別依委派而定。
class Program
{
static IAsyncResult asyncResult;
static void Main(string[] args)
{
AsyncTest test = new AsyncTest();
SalaryEventHandler dele = test.YearlySalary;
asyncResult = dele.BeginInvoke(100000, 15, 100000, null, null);
decimal val = GetResult();
Console.WriteLine(val);
Console.ReadLine();
}
static decimal GetResult()
{
AsyncResult result = (AsyncResult)asyncResult;
SalaryEventHandler salDel = (SalaryEventHandler)result.AsyncDelegate;
return salDel.EndInvoke(asyncResult);
}
}
class Program
{
static void Main(string[] args)
{
AsyncTest test = new AsyncTest();
SalaryEventHandler dele = test.YearlySalary;
dele.BeginInvoke(100000, 15, 100000, GetResultCallBack, null);
Console.ReadLine();
}
static void GetResultCallBack(IAsyncResult asyncResult)
{
AsyncResult result = (AsyncResult)asyncResult;
SalaryEventHandler salDel = (SalaryEventHandler)result.AsyncDelegate;
decimal val = salDel.EndInvoke(asyncResult);
Console.WriteLine(val);
}
}
dele.BeginInvoke(100000, 15, 100000, GetResultCallBack, 2000); // 傳入 int
static void GetResultCallBack(IAsyncResult asyncResult)
{
int para = (int)asyncResult.AsyncState; // 取回 2000
Console.WriteLine(para);
}
若目標方法拋出例外,CLR 會在 EndInvoke 時重新擲出原始例外:
public decimal YearlySalary(decimal salary, int monthCount, decimal bonus)
{
throw new Exception("error"); // 模擬錯誤
return salary * monthCount + bonus;
}
static void GetResultCallBack(IAsyncResult asyncResult)
{
try
{
decimal val = salDel.EndInvoke(asyncResult); // 這裡捕獲
Console.WriteLine(val);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // 輸出 "error"
}
}
遵循 .NET 命名慣例:Begin+方法名 / End+方法名。
public class AsyncTest
{
private delegate string AsyncEventHandler(string name);
private AsyncEventHandler _Async;
public string Hello(string name) => "Hello:" + name;
// BeginHello:參數同 Hello,補上 AsyncCallback & object state,回傳 IAsyncResult
public virtual IAsyncResult BeginHello(string name, AsyncCallback callback, object state)
{
AsyncEventHandler del = Hello;
this._Async = del;
return del.BeginInvoke(name, callback, state);
}
// EndHello:參數為 IAsyncResult,回傳 string
public virtual string EndHello(IAsyncResult asyncResult)
{
if (asyncResult == null) throw new ArgumentNullException(nameof(asyncResult));
if (this._Async == null) throw new ArgumentException("_Async");
string val = string.Empty;
try { val = this._Async.EndInvoke(asyncResult); }
finally { this._Async = null; }
return val;
}
}
class Program
{
static void Main(string[] args)
{
AsyncTest test = new AsyncTest();
AsyncCallback callback = new AsyncCallback(OnHelloCallback);
test.BeginHello("Andy Huang", callback, test);
Console.ReadLine();
}
static void OnHelloCallback(IAsyncResult asyncResult)
{
AsyncTest obj = (AsyncTest)asyncResult.AsyncState;
string val = obj.EndHello(asyncResult);
Console.WriteLine(val);
}
}
本文透過反編譯與實戰範例,完整展示了 .NET 委派型別的非同步機制內幕:Invoke 為同步阻塞,BeginInvoke/EndInvoke 成對實現真正的非同步執行緒池調度,支援輪詢與回呼兩種模式,並能透過 AsyncState 傳遞上下文、在 EndInvoke 統一處理例外。最後示範如何為自己的類別封裝符合框架規範的非同步介面,為後續進階非同步模式(APM、EAP、TAP)奠定基礎。