线程池ThreadPool.QueueUserWorkItem的行为出现意外情况

6
请查看下面的代码示例:
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,而执行显然是相互独立的。
难道不是不同的对象与不同的线程一起使用吗?

又是一天,又有一个人不知道如何正确使用闭包。难怪Java如此不愿意添加它们... - leppie
1
Leppie 的意思是你捕获了变量 s 而不是它的值。当线程执行时,迭代已经完成,s 保留了它的最后一个值。 - Zarat
我觉得我迷失了... 顺便问一下,什么是闭包问题? - Shekhar_Pro
@leppie 谢谢你的讽刺...我希望你现在感觉更好了...现在,我们能否看到你的超级极客技能,并给出一个更好的解释呢! :) - Danish Khan
1
@Leppie 没关系,先生..我们不要争论了,我为我的评论感到抱歉。 :) 现在,也许你可以解释一下“浮点数相等性”问题对应的是什么.. ;). 只是开个玩笑!! - Danish Khan
显示剩余4条评论
1个回答

11
这是旧的修改闭包问题。你可能想查看线程池 - 可能存在线程执行顺序问题,以获取类似的问题,并查看Eric Lippert的博客文章关闭循环变量被认为是有害的了解此问题。
基本上,您在那里拥有的Lambda表达式捕获的是变量s,而不是在声明Lambda时变量的值。因此,对变量值进行的后续更改对代理是可见的。 RunCount方法将运行取决于实际执行委托时变量s引用的实例(其值)的Sample实例。
另外,由于异步执行委托(编译器实际上重用相同的委托实例),无法保证执行点的这些值将是什么。您目前看到的是foreach循环在主线程上完成之前任何委托调用(预期需要在线程池上安排任务需要时间)。因此,所有工作项都会看到循环变量的“最终”值。但这绝非保证。尝试在循环内插入合理持续时间的Thread.Sleep,您将看到不同的输出。
通常的解决方法是:
  1. 在循环体内部引入另一个变量。
  2. 将该变量分配给当前循环变量的值。
  3. 捕获“复制”变量而不是循环变量在Lambda内部

    foreach (Sample s in arrSample)
    {
        Sample sCopy = s;
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount());
    }
    
    现在每个工作项"拥有"循环变量的特定值。
    在这种情况下的另一个选项是完全避免捕获任何东西:
    ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);
    

谢谢@ani,这很有道理。难怪我被喷了!我应该知道的.. :) - Danish Khan
就此问题而言,C#团队认识到他们使这种错误变得太容易了,并在C# 5中引入了一个破坏性变化来解决它。因此,在后续版本的C#中,这个问题将不再存在。 - StriplingWarrior

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接