带委托的QueueUserWorkItem不起作用,但WaitCallBack确实有效

时间:2012-10-19 15:45:48

标签: c# multithreading delegates

在下面的问题中,我发现了以类型安全的方式调用QueueUserWorkItem的巧妙技巧,您可以在其中传递委托而不是WaitCallBack和对象。但是,它并不像人们期望的那样工作。

What's the difference between QueueUserWorkItem() and BeginInvoke(), for performing an asynchronous activity with no return types needed

以下是一些演示此问题的示例代码和输出。

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. 使用closure / delegate方法时,为什么i没有传递正确的值?
  2. 我到底怎么会变成10岁?在循环中,它只有值0-9对吗?

2 个答案:

答案 0 :(得分: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>,该值在此时固定。

请注意in C# 5.0, the behavior for foreach was changed because it happens so often (where the closure closes over the loop) and causes a number of people a lot of headaches(但不是for,就像您正在使用的那样)。

如果您想利用这一事实,可以致电Range method上的Enumerable class使用foreach

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

答案 1 :(得分:2)

这是因为如何捕获变量:委托将在实际执行时获取i的值,而不是在声明时,因此到那时他们都是10.尝试副本到一个局部变量:

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