同步 I/O 操作的特點是速度慢且不可預期。當應用程式執行同步 I/O 時,呼叫執行緒會被封鎖,無法進行其他有用的工作。若該執行緒是 UI 執行緒,整個應用程式會凍結、停止回應使用者輸入。為了提高擴展性,開發人員常建立更多執行緒,但每個執行緒都帶來核心物件、堆疊空間、內容切換等額外開銷,反而降低擴展性。
若應用程式想保持回應能力、提高擴展性與吞吐量,就不應同步執行 I/O,而應採用 Common Language Runtime (CLR) 的非同步程式設計模型 (APM) 來進行非同步 I/O 操作。
SqlCommand 的 BeginExecuteNonQuery 等方法,內部封裝了非同步 I/O。BeginXxx/EndXxx 方便呼叫者使用 APM 模式。CLR APM 的核心是 IAsyncResult 介面,定義四個唯讀屬性:
AsyncWaitHandle:用於「等待直到完成」技巧的 WaitHandle。IsCompleted:用於「輪詢」技巧的布林值。AsyncState:用於「回呼」技巧的使用者狀態物件。CompletedSynchronously:幾乎不使用,指示是否同步完成。呼叫 BeginXxx 時,必須內部建構實作 IAsyncResult 的物件,識別剛啟動的非同步操作狀態。呼叫端透過 EndXxx 取得結果或例外。
文章提供兩個基底類別:
Stream.BeginWrite)。內部以 m_CompletedState 追蹤狀態,延遲建立 ManualResetEvent 以避免核心物件開銷。AsyncResultNoResult,增加泛型結果欄位 m_result 與對應的 SetAsCompleted/EndInvoke 多載。關鍵設計:只有當呼叫端真正存取 AsyncWaitHandle 或在操作完成前呼叫 EndInvoke 時,才建立 ManualResetEvent。最常見的使用方式是指定 AsyncCallback,此時完全不需要事件物件。
範例展示如何使用 AsyncResult<DateTime> 將同步方法 DoTask 包裝為非同步 BeginDoTask/EndDoTask:
BeginDoTask 建立 AsyncResult<DateTime>,利用執行緒池執行 DoTaskHelper。DoTaskHelper 在 try 區塊呼叫同步方法,成功呼叫 SetAsCompleted(result, false),失敗呼叫 SetAsCompleted(exception, false)。EndDoTask 轉型後呼叫 EndInvoke,等待完成並回傳結果或拋出例外。程式碼示範:
EndDoTask 封鎖等待。IsCompleted。AsyncCallback,完成時自動觸發。與委派的 BeginInvoke/EndInvoke 比較,自訂實作因避免無條件建立 ManualResetEvent,效能更佳。測試 10 萬次迴圈顯示自訂實作耗時較短。
理解 IAsyncResult 內部運作有助於建構高效能應用程式。本文著重於計算密集型任務(使用執行緒池),後續將探討如何套用於 I/O 密集型操作。