请检查下面的代码示例:

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,而执行显然是相互独立的。
不同的对象是否与不同的线程一起使用?

最佳答案

这是旧的修改闭包问题。你可能想看:Threadpools - possible thread execution order problem来回答一个类似的问题,还有eric lippert的博客文章Closing over the loop variable considered harmful来理解这个问题。
基本上,您得到的lambda表达式是捕获变量s而不是声明lambda时变量的值。因此,对变量值所做的后续更改对委托可见。运行Sample方法的RunCount实例将取决于变量s(其值)在委托实际执行时引用的实例。
另外,由于委托(编译器实际上重用同一个委托实例)是异步执行的,因此不能保证每次执行时这些值是什么。您目前看到的是foreach循环在任何委托调用之前在主线程上完成(这是意料之中的-在线程池上调度任务需要时间)。所以所有的工作项最终都得到了循环变量的“final”值。但这无论如何都不能保证;尝试在循环中插入一个合理的持续时间Thread.Sleep,您将看到一个不同的输出。
通常的解决方法是:
在循环体中引入另一个变量。
将该变量赋给循环变量的当前值。
捕获“copy”变量,而不是lambda中的循环变量。

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

现在,每个工作项“拥有”循环变量的特定值。
在这种情况下,另一个选择是通过不捕获任何内容来完全回避问题:
ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);

07-28 02:06
查看更多