var actions = new List {
() => Console.WriteLine("A-1"),
() => Console.WriteLine("A-2"),
() => Console.WriteLine("A-3"),
() => Console.WriteLine("A-4")
};
foreach (var act in actions) {
ThreadPool.QueueUserWorkItem(_ => act());
}
Console.ReadKey();
// 輸出可能全為 "A-4" (視執行時機而定)
foreach 的迭代變數 act 在編譯器生成的類別中為**單一實例欄位**。act 指向最後一個元素。| 方法 | 程式碼 | 原理 | 優缺點 |
|---|---|---|---|
| 1. 區域變數複製 (推薦) |
| 區域變數生命週期限於迴圈迭代,每次捕獲不同實例 | 簡潔、零開銷、C# 5+ foreach 語意已修正 (見下) |
| 2. Thread.Sleep(1) (不推薦) |
| 延遲主執行緒,增加捕獲正確值的機率 | 非確定性、浪費時間、高負載失效 |
| 3. 傳遞 state 參數 |
| 利用 QueueUserWorkItem 的 object state 參數傳值 | 較囉嗦,但語意明確,適用於舊版 API |
| 4. Task.Run (現代替代) |
| Task.Run 接受 Action 直接排程,避免閉包問題 | 需 .NET 4.5+,回傳 Task 可 await,推薦新專案 |
自 C# 5 (VS2012) 起,foreach 迭代變數**每次迭代重新宣告**,因此直接捕獲 act 即可正確運作:
// C# 5+ 編譯器自動轉換為:
foreach (var tmp in actions) { // tmp 為每次迴圈新變數
ThreadPool.QueueUserWorkItem(_ => tmp());
}
但 若專案語言版本設為 C# 4 或更舊,或使用 `for` 迴圈索引變數,仍會遇到閉包陷阱。
for (int i = 0; i < 4; i++) {
ThreadPool.QueueUserWorkItem(_ => Console.WriteLine(i));
}
// 輸出四次 4
修正:
for (int i = 0; i < 4; i++) {
int idx = i; // 複製到區域變數
ThreadPool.QueueUserWorkItem(_ => Console.WriteLine(idx));
}
// 輸出 0,1,2,3 (順序不定)
Task.Run / Parallel.ForEach / await,避免手動操作 ThreadPool。ThreadPool.QueueUserWorkItem 時,**在迴圈內建立區域變數複製值** (`var tmp = act;`)。CS1998 (非同步方法缺少 await) 並將警告視為錯誤,及早發現潛在問題。ThreadPool.QueueUserWorkItem 閉包陷阱是 C# 非同步程式設計的經典坑。理解「捕獲變數而非值」的語意、掌握 C# 5+ foreach 語意變更,並養成「區域變數複製」習慣,即可徹底避免此類 Bug。現代程式碼建議全面遷移至 Task 為基礎的 TPL 模型,獲得更安全、可組合的非同步流程控制。