19

我刚刚遇到以下行为:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i.ToString());
    });
}

将导致一系列“错误:x”,其中大部分 x 等于 50。

相似地:

var a = "Before";
var task = new Task(() => Debug.Print("Using value: " + a));
a = "After";
task.Start();

将导致“使用值:之后”。

这显然意味着 lambda 表达式中的连接不会立即发生。在声明表达式时,如何在 lambda 表达式中使用外部变量的副本?以下不会更好(我承认这不一定是不连贯的):

var a = "Before";
var task = new Task(() => {
    var a2 = a;
    Debug.Print("Using value: " + a2);
});
a = "After";
task.Start();
4

4 回答 4

31

This has more to do with lambdas than threading. A lambda captures the reference to a variable, not the variable's value. This means that when you try to use i in your code, its value will be whatever was stored in i last.

To avoid this, you should copy the variable's value to a local variable when the lambda starts. The problem is, starting a task has overhead and the first copy may be executed only after the loop finishes. The following code will also fail

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        var i1=i;
        Debug.Print("Error: " + i1.ToString());
    });
}

As James Manning noted, you can add a variable local to the loop and copy the loop variable there. This way you are creating 50 different variables to hold the value of the loop variable, but at least you get the expected result. The problem is, you do get a lot of additional allocations.

for (var i = 0; i < 50; ++i) {
    var i1=i;
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i1.ToString());
    });
}

The best solution is to pass the loop parameter as a state parameter:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(o => {
        var i1=(int)o;
        Debug.Print("Error: " + i1.ToString());
    }, i);
}

Using a state parameter results in fewer allocations. Looking at the decompiled code:

  • the second snippet will create 50 closures and 50 delegates
  • the third snippet will create 50 boxed ints but only a single delegate
于 2012-06-15T11:02:18.277 回答
4

那是因为您在新线程中运行代码,而主线程立即继续更改变量。如果立即执行 lambda 表达式,则使用任务的全部意义都将丢失。

线程在创建任务时没有获得自己的变量副本,所有任务都使用相同的变量(实际上存储在方法的闭包中,它不是局部变量)。

于 2012-06-15T10:58:12.323 回答
3

Lambda 表达式捕获的不是外部变量的值,而是对其的引用。这就是您确实看到50After在您的任务中的原因。

要解决此问题,请在您的 lambda 表达式之前创建它的副本以按值捕获它。

这种不幸的行为将由带有 .NET 4.5 的 C# 编译器修复,直到那时您需要忍受这种奇怪。

例子:

    List<Action> acc = new List<Action>();
    for (int i = 0; i < 10; i++)
    {
        int tmp = i;
        acc.Add(() => { Console.WriteLine(tmp); });
    }

    acc.ForEach(x => x());
于 2012-06-15T11:00:51.700 回答
2

根据定义,Lambda 表达式是惰性求值的,因此它们在实际调用之前不会被求值。在您的情况下,由任务执行。如果您在 lambda 表达式中关闭本地,则将反映执行时本地的状态。这就是你所看到的。您可以利用这一点。例如,您的 for 循环确实不需要为每次迭代都使用新的 lambda,假设为了示例所描述的结果是您想要编写的结果

var i =0;
Action<int> action = () => Debug.Print("Error: " + i);
for(;i<50;+i){
    Task.Factory.StartNew(action);
}

另一方面,如果您希望它实际打印"Error: 1"..."Error 50",您可以将上面的内容更改为

var i =0;
Func<Action<int>> action = (x) => { return () => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action(i));
}

第一个关闭i并将使用i执行操作时的状态,并且状态通常是循环完成后的状态。在后一种情况下i,因为它作为参数传递给函数而被急切地评估。然后这个函数返回一个Action<int>传递给StartNew.

因此,设计决策使惰性评估和急切评估成为可能。懒惰是因为本地人被关闭并且急切地因为您可以通过将本地人作为参数传递或如下所示声明另一个具有更短范围的本地人来强制执行本地人

for (var i = 0; i < 50; ++i) {
    var j = i;
    Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}

以上所有内容对于 Lambda 都是通用的。在特定情况下,StartNew实际上有一个重载执行第二个示例所做的操作,因此可以简化为

var i =0;
Action<object> action = (x) => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action,i);
}
于 2012-06-15T11:06:25.290 回答