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  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 ,而执行显然是相互独立的。

不同的对象是否与不同的线程一起使用?

这是旧的修改后的闭包问题。 您可能希望查看: Threadpools -类似问题的可能的线程执行顺序问题 ,以及Eric Lippert的博客文章Closing over loop variable被认为对理解该问题有害 。

从本质上讲,你所获得的lambda表达式是捕获变量 s而不是声明lambda的变量 。 因此,委托可以看到对变量值的后续更改。 运行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);