首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

非同步匿名函數捕捉區域變數的陷阱與三種解決策略

     編輯:關於.NET

經典 Bug:迴圈變數在多執行緒中「同步」失效

看看這段看似合理的程式碼:

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);

方案二示意圖

參數在呼叫時壓入堆疊,與外部變數完全無關,同樣能得到正確結果。

解決方案三:共享可變狀態配合同步(Queue 模式)

若需保證處理順序(如 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 當參數傳入避免閉包。

Queue 同步輸出範例

核心心法:理解「捕捉時機」與「執行時機」的脫節

Bug 的本質在於:捕捉發生在建立委派時,執行發生在未來某個平行宇宙。解決之道只有一個——確保傳給執行緒的資料在傳遞後不再被主執行緒修改:

  • 方案一:每次建立新容器複製值(值型別自動複製)。
  • 方案二:用參數傳值切斷參考。
  • 方案三:刻意共享並同步(需承擔效能代價)。

甚至連參考型別的屬性修改也可能踩雷,唯有深刻理解閉包機制,才能在多執行緒世界裡不「莫名其妙掉褲子」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved