使用委托的QueueUserWorkItem无法工作,但WaitCallBack可以正常工作。

4
在下面的问题中,我发现了一种调用QueueUserWorkItem的巧妙方法,以类型安全的方式传递委托而不是WaitCallBack和对象。然而,它并不像人们期望的那样工作。
这里有一些示例代码和输出,演示了这个问题。
链接:QueueUserWorkItem()和BeginInvoke()有什么区别,用于执行不需要返回类型的异步操作
for (int i = 0; i < 10; ++i)
{
    // doesn't work - somehow DoWork is invoked with i=10 each time!!!
    ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", i); });

    // not type safe, but it works
    ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork), Tuple.Create("    WCB", i));
}

void DoWork(string s, int i)
{
    Console.WriteLine("{0} - i:{1}", s, i);
}

void DoWork(object state)
{
    var t = (Tuple<string, int>)state;
    DoWork(t.Item1, t.Item2);
}

这里是输出结果:

closure - i:10
    WCB - i:0
closure - i:10
    WCB - i:2
    WCB - i:3
closure - i:10
    WCB - i:4
closure - i:10
    WCB - i:5
closure - i:10
    WCB - i:6
closure - i:10
    WCB - i:7
closure - i:10
    WCB - i:8
closure - i:10
    WCB - i:9
    WCB - i:1
closure - i:10

请注意,使用闭包调用QueueUserWorkitem时,每次调用i都为10,但是使用WaitCallBack时可以得到正确的值0-9。
所以我的问题是:
1.为什么使用闭包/委托的方式时没有传递正确的i值?
2.i怎么可能变成10?在循环中,它只有0-9的值,对吗?

循环变量捕获,再次发生。在Stack Overflow上搜索该术语。 - usr
或者,您可以找到其中一个重复项并投票关闭。这将是更有效的做法。 - casperOne
2个回答

6
你的两个问题的答案都与创建匿名方法时的闭包作用域相关。
当你执行以下代码时:
// Closure for anonymous function call begins here.
for (int i = 0; i < 10; ++i)
{
    // i is captured
    ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", i); });
}

你正在整个循环中捕获 i。这意味着你非常快地排队了十个线程,并且在它们开始时,闭包已经捕获了 i 的值为 10。

为了解决这个问题,你可以通过在循环内部引入一个变量来减少闭包的作用域,如下所示:

for (int i = 0; i < 10; ++i)
{
    // Closure extends to here.
    var copy = i;

    // **copy** is captured
    ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", copy); });
}

在这里,闭包不会超出循环范围,只会作用于其中的值。

话虽如此,对于第二次调用QueueUserWorkItem,它可以产生所需的结果,因为你已经在将委托排队时创建了Tuple<T1, T2>,此时该值已经被固定。

请注意,在C#5.0中,foreach的行为发生了变化,因为它经常发生(闭包覆盖循环),给许多人带来了很多麻烦(但是像你使用的那样的for没有改变)。

如果您想利用这个事实,可以在Enumerable上调用Range方法来使用foreach

foreach (int i in Enumerable.Range(0, 10))
{
    // Closure for anonymous function call begins here.
    ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", i); });
}

2

这是因为变量的捕获方式:委托将在实际执行时获取i的值,而不是在声明时获取,因此到那时它们都是10。尝试将其复制到本地变量中:

for (int i = 0; i < 10; ++i)
{
    int j = i;        
    ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", j); });

我明白了,谢谢!如果我不知道这个,看到那段代码会想:“什么鬼 - 为什么要复制?” - Jesse

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