看看這段看似合理的程式碼:
static void Main(string[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(delegate() {
Thread.Sleep(new Random().Next(1, 10000));
Console.Write(i + ", ");
});
t.Start();
}
}
預期輸出應是 0~9 的隨機順序,實際卻全印出 10, 10, 10...。原因在於匿名委派捕捉的是 i 的參考,而非當下的值。當執行緒真正跑起來時,迴圈早已結束,i 變成了 10。
用 Reflector 看編譯後的 IL 就一目瞭然:編譯器會產生一個「顯示類別」來封裝被捕捉的區域變數,匿名方法實際上存取的是這個類別的欄位。所有執行緒共享同一個顯示類別實例,自然看到同一個最終值。

在迴圈內部宣告區域變數 j,每次迭代都會建立新的顯示類別實例,j 的值被複製進去:
for (int i = 0; i < 10; i++) {
int j = i; // 每次迭代新建 j
Thread t = new Thread(delegate() {
Thread.Sleep(new Random().Next(1, 10000));
Console.Write(j + ", ");
});
t.Start();
}

輸出即為預期的亂序 0~9。缺點是意圖不明顯,建議加註解警告維護者勿動。
利用 Thread.Start(object) 或 ThreadPool.QueueUserWorkItem 將值當參數傳入,匿名函數只用參數 arg:
ThreadPool.QueueUserWorkItem(delegate(object arg) {
Thread.Sleep(new Random().Next(1, 1000));
Console.Write(arg + ", ");
}, i);

參數在呼叫時壓入堆疊,與外部變數完全無關,同樣能得到正確結果。
若需保證處理順序(如 Boss 要求輸出 0,1,2...9),必須讓執行緒共享一個 Queue<int> 並加鎖:
Queue<int> q = new Queue<int>();
for (int i = 0; i < 10; i++) {
q.Enqueue(i);
Thread t = new Thread(delegate() {
Thread.Sleep(new Random().Next(1, 10000));
lock (q) {
if (q.Count > 0)
Console.WriteLine(Thread.CurrentThread.Name + ": do " + q.Dequeue());
}
});
t.Name = "線程" + i;
t.Start();
}
輸出會依序列出 0~9,但因鎖競爭降低平行度。也可將 Queue 當參數傳入避免閉包。

Bug 的本質在於:捕捉發生在建立委派時,執行發生在未來某個平行宇宙。解決之道只有一個——確保傳給執行緒的資料在傳遞後不再被主執行緒修改:
甚至連參考型別的屬性修改也可能踩雷,唯有深刻理解閉包機制,才能在多執行緒世界裡不「莫名其妙掉褲子」。