2011-01-20 77 views
6

請檢查下面的代碼示例:意外行爲ThreadPool.QueueUserWorkItem

public class Sample 
{ 
    public int counter { get; set; } 
    public string ID; 
    public void RunCount() 
    { 
     for (int i = 0; i < counter; i++) 
     { 
      Thread.Sleep(1000); 

      Console.WriteLine(this.ID + " : " + i.ToString()); 
     } 
    } 
} 

class Test 
{ 
    static void Main() 
    { 
     Sample[] arrSample = new Sample[4]; 

     for (int i = 0; i < arrSample.Length; i++) 
     { 
      arrSample[i] = new Sample(); 
      arrSample[i].ID = "Sample-" + i.ToString(); 
      arrSample[i].counter = 10; 
     } 

     foreach (Sample s in arrSample) 
     { 
      ThreadPool.QueueUserWorkItem(callback => s.RunCount()); 
     } 

     Console.ReadKey(); 
    } 

} 

此示例的預期輸出應該是這樣的:

Sample-0 : 0 
Sample-1 : 0 
Sample-2 : 0 
Sample-3 : 0 
Sample-0 : 1 
Sample-1 : 1 
Sample-2 : 1 
Sample-3 : 1 
. 
. 
. 

但是,當您運行此代碼,它會顯示這樣的代替:

Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 1 
Sample-3 : 1 
Sample-3 : 0 
Sample-3 : 2 
Sample-3 : 2 
Sample-3 : 1 
Sample-3 : 1 
. 
. 
. 

我可以理解,廣告執行可能會有所不同,因此計數不會以循環方式增加。然而,我不明白,爲什麼所有的ID都顯示爲Sample-3,而執行顯然是彼此獨立的。

Arent不同的對象被用於不同的線程?

+0

另一天,另一個人不知道如何正確使用倒閉。難怪Java是如此不願意添加它們...... – leppie 2011-01-20 07:54:42

+1

leppie說的是你捕獲變量`s`而不是它的值。線程執行時,迭代完成,`s`保留最後一個值。 – Zarat 2011-01-20 07:59:13

回答

10

這是舊的修改過的閉包問題。你可能想看看:Threadpools - possible thread execution order problem對於一個類似的問題,和Eric Lippert的博客帖子Closing over the loop variable considered harmful瞭解這個問題。

從本質上講,你lambda表達式到了那裏被捕獲變量s而不是在拉姆達聲明的點變量的值。因此,後續對變量的值進行的更改是對代表可見。 RunCount方法將運行的Sample的實例將取決於代理實際執行時由變量s(其值)引用的實例。

此外,由於委託(編譯器實際上重用相同的委託實例)是異步執行的,因此不能保證這些值在每次執行時都處於什麼位置。你目前看到的是foreach循環在主線程上完成之前的任何委託調用(預計 - 需要時間來計劃線程池上的任務)。所以全部工作項目結束了循環變量的'最終'值。但是這不能以任何方式保證;嘗試在循環內插入合理的持續時間Thread.Sleep,您將看到不同的輸出。


通常的解決方法是:

  1. 介紹另一個變量內部循環體。
  2. 將該變量分配給循環變量的當前值。
  3. 捕獲'copy'變量而不是lambda內的循環變量。

    foreach (Sample s in arrSample) 
    { 
        Sample sCopy = s; 
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount()); 
    } 
    

現在每個工作項目「擁有」的循環變量的特定值。


在這種情況下,另一種選擇是通過不捕捉任何完全迴避這個問題:

ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);