在下面的问题中,我发现了一个以类型安全的方式调用 QueueUserWorkItem 的巧妙技巧,您可以传递一个委托(delegate)而不是 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。

所以我的问题是:
  • 为什么在使用闭包/委托(delegate)方式时传递的 i 值不正确?
  • 我到底是怎么到 10 岁的?在循环中,它只有 0-9 的值,对吗?
  • 最佳答案

    当您创建匿名方法时,您的两个问题的答案都与闭包的范围有关。

    当你这样做时:

    // Closure for anonymous function call begins here.
    for (int i = 0; i < 10; ++i)
    {
        // i is captured
        ThreadPool.QueueUserWorkItem(delegate { DoWork("closure", i); });
    }
    

    您正在整个循环中捕获 i。这意味着您很快将 10 个线程排队,当它们开始时,闭包已捕获 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 的第二次调用产生了所需的结果,因为您在委托(delegate)排队时创建了 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); });
    }
    

    10-08 01:53