ThreadPool.QueueUserWorkItem的意外行为

时间:2011-01-20 07:50:49

标签: c# .net multithreading asynchronous threadpool

请检查以下代码示例:

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

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

1 个答案:

答案 0 :(得分:10)

这是旧的修改后的闭包问题。您可能需要查看:Threadpools - possible thread execution order problem以查找类似问题,以及Eric Lippert的博客文章Closing over the loop variable considered harmful以了解该问题。

基本上,你所获得的lambda表达式是在声明lambda的点上捕获变量 s而不是变量的 。因此,对代理可见的变量的值的后续更改Sample方法将运行的RunCount实例将取决于委托实际执行时变量s(其值)引用的实例

此外,由于委托(编译器实际上重用了相同的委托实例)正在异步执行,因此无法保证每次执行时这些值是什么。您目前看到的是foreach循环在任何委托调用之前在主线程上完成(预期 - 在线程池上调度任务需要时间) )。所以所有工作项最终会找到循环变量的'final'值。但这无法保证;尝试在循环中插入一个合理的持续时间Thread.Sleep,您将看到不同的输出。


通常的解决办法是:

  1. 循环体内引入另一个变量
  2. 将该变量分配给循环变量的当前值。
  3. 捕获'copy'变量而不是lambda中的循环变量。

    foreach (Sample s in arrSample)
    {
        Sample sCopy = s;
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount());
    }
    
  4. 现在每个工作项“拥有”循环变量的特定值。


    在这种情况下的另一个选择是完全避免捕获任何东西:

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