.NET 執行緒池內建兩類工作者:**Worker Threads**(CPU 密集)與 **I/O Threads**(非同步 I/O 完成)。理解兩者差異、正確使用 Begin/End、async/await、Task.Run,是寫出高吞吐、低延遲、不阻塞執行緒池服務的關鍵。本文從核心概念、API 演進、實戰範例三維度深入剖析。
| 特性 | Worker Threads | I/O Threads |
|---|---|---|
| 用途 | 計算、邏輯處理、同步阻塞呼叫 | 等待硬體 I/O 完成(磁碟、網路、資料庫) |
| 排程來源 | ThreadPool.QueueUserWorkItem、Task.Run、Parallel.For | OS I/O 完成通知(IOCP)→ 執行緒池排程回呼 |
| 阻塞成本 | 高:佔用一顆核心、阻塞排程器 | 極低:不佔用 Worker、由驅動程式/硬體管理等待 |
| 典型 API | 同步方法、Task.Run | FileStream.BeginRead、HttpClient.GetAsync、SqlCommand.ExecuteReaderAsync |
fileStream.BeginRead(buffer, 0, len, callback, state)。callback(即 EndReadCallback)。fileStream.EndRead(asyncResult) 取得實際讀取位元組數。全程 **無 Worker Thread 等待**,實現真正的非阻塞並發。
// 舊 APM 模式
fileStream.BeginRead(buf, 0, buf.Length, ar => {
int read = fileStream.EndRead(ar);
// 處理資料
}, null);
// 現代 TAP 模式(編譯器自動生成狀態機,同樣使用 I/O Threads 完成)
int read = await fileStream.ReadAsync(buf, 0, buf.Length);
// await 之後的續作預設由捕獲的 SynchronizationContext 或 TaskScheduler 執行
// 若為 Console/BackgroundService,續作回到 ThreadPool(Worker 或 I/O 視完成來源)
核心誤區:await 不等於「另開執行緒」。它是「掛起當前方法、註冊續作、歸還執行緒」。真正的並行來自 I/O 硬體非同步,而非執行緒切換。
public static async Task ProcessLargeFileAsync(string path, CancellationToken ct)
{
// 關鍵:FileOptions.Asynchronous / FileOptions.SequentialScan
await using FileStream fs = new FileStream(path,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 81920, FileOptions.Asynchronous | FileOptions.SequentialScan);
byte[] buffer = ArrayPool.Shared.Rent(81920);
try
{
int bytesRead;
while ((bytesRead = await fs.ReadAsync(buffer, ct)) > 0)
{
// 處理 buffer[0..bytesRead)
await ProcessChunkAsync(buffer.AsMemory(0, bytesRead), ct);
}
}
finally
{
ArrayPool.Shared.Return(buffer);
}
}
FileOptions.Asynchronous 確保 Windows 使用 IOCP;Linux/macOS 透過 epoll/kqueue 同樣達到非阻塞。
private static readonly HttpClient _http = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30),
DefaultRequestVersion = HttpVersion.Version20
};
public static async Task FetchWithRetryAsync(string url, int maxRetry = 3, CancellationToken ct = default)
{
for (int i = 0; ; i++)
{
try
{
using HttpResponseMessage resp = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
// 串流式讀取,避免大回應佔記憶體
return await resp.Content.ReadAsStringAsync(ct);
}
catch (Exception ex) when (i < maxRetry - 1 && IsTransient(ex))
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)), ct); // 指數退避
}
}
}
static bool IsTransient(Exception ex) => ex is HttpRequestException or TaskCanceledException or TimeoutException;
關鍵點:HttpClient 為單例重用(避免 Socket 耗盡)、HttpCompletionOption.ResponseHeadersRead 讓標頭讀完即返回、串流式讀取 Body。
// 取得可用執行緒數
ThreadPool.GetAvailableThreads(out int worker, out int io);
Console.WriteLine($"Worker: {worker}, IO: {io}");
// 設定最小執行緒(啟動時預熱,避免冷啟動延遲)
ThreadPool.SetMinThreads(200, 200); // 視負載調整,通常設為 CPU 核心數 × 50~100
// 監控佇列長度(.NET 6+)
// 透過 EventSource "System.Threading.ThreadPool" 或自訂 Metrics
| 反模式 | 後果 | 修正 |
|---|---|---|
Task.Run(() => syncIO.Call()) | 佔用 Worker 模擬非同步,池耗盡 | 改用真正的非同步 API(ReadAsync、GetAsync) |
.Result / .Wait() 同步阻塞 | 死鎖風險、阻塞 Worker | 全鏈路 async/await,頂層 await 或 GetAwaiter().GetResult() 僅限入口 |
| 大量小檔同步讀寫 | Worker 切換開銷大 | 批次非同步、FileOptions.Asynchronous、ArrayPool 緩衝區 |
忽略 CancellationToken | 無法優雅取消、資源洩漏 | 全程透傳 ct,ThrowIfCancellationRequested |
[Application] --BeginRead/ReadAsync--> [ThreadPool IO Queue] --IRP--> [Kernel/Driver]
|
[Completion Port] <--Completion Packet-- [Hardware/Driver] <--|
|
v
[ThreadPool IO Thread] --> [User Callback / await continuation]
await ReadAsync()、Parallel.ForEach / Task.Run 分流。.Result、.Wait()、Task.Run(sync)。ArrayPool、MemoryPool、HttpClient、DbConnection 重用,減少 GC 壓力。深入理解 I/O Threads 與 IOCP 機制,配合現代 async/await、ValueTask、ArrayPool、CancellationToken 工具箱,即能建構出在高併發、高 I/O 負載下依然穩定、延遲可控的 .NET 服務。