我刚刚遇到以下行为:
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();
答案 0 :(得分:26)
这与lambdas有关,而不是线程。 lambda捕获对变量的引用,而不是变量的值。这意味着当您尝试在代码中使用 i 时,其值将是最后存储在 i 中的值。
为避免这种情况,您应该在lambda启动时将变量的值复制到局部变量。问题是,启动任务有开销,第一个副本可能只在循环结束后执行。 以下代码也将失败
for (var i = 0; i < 50; ++i) {
Task.Factory.StartNew(() => {
var i1=i;
Debug.Print("Error: " + i1.ToString());
});
}
正如James Manning所说,你可以在循环中添加一个局部变量并在那里复制循环变量。这样您就可以创建50个不同的变量来保存循环变量的值,但至少可以得到预期的结果。问题是,你确实得到了很多额外的分配。
for (var i = 0; i < 50; ++i) {
var i1=i;
Task.Factory.StartNew(() => {
Debug.Print("Error: " + i1.ToString());
});
}
最佳解决方案是将loop参数作为状态参数传递:
for (var i = 0; i < 50; ++i) {
Task.Factory.StartNew(o => {
var i1=(int)o;
Debug.Print("Error: " + i1.ToString());
}, i);
}
使用状态参数可以减少分配。查看反编译代码:
答案 1 :(得分:4)
那是因为您在新线程中运行代码,并且主线程立即继续更改变量。如果lambda表达式立即执行,则使用任务的整个过程都将丢失。
线程在创建任务时没有获得自己的变量副本,所有任务都使用相同的变量(实际上存储在方法的闭包中,它不是局部变量)。 / p>
答案 2 :(得分:3)
Lambda表达式不会捕获外部变量的值,而是捕获它的引用。这就是您在任务中看到50
或After
的原因。
要解决此问题,请在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());
答案 3 :(得分:1)
根据定义,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
并在执行Action时使用状态i
,状态通常是循环结束后的状态。在后一种情况下,i
被急切地评估,因为它作为参数传递给函数。然后,此函数返回Action<int>
,并传递给StartNew
。
因此,设计决策使懒惰的评估和急切的评估成为可能。懒惰,因为你可以通过将它们作为参数传递来强制执行本地人,或者如下所示声明另一个具有较短范围的本地,因此迫切需要本地人关闭
for (var i = 0; i < 50; ++i) {
var j = i;
Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}
以上所有内容都适用于Lambdas。在StartNew
的特定情况下,实际上有一个重载可以执行第二个示例所做的事情,可以简化为
var i =0;
Action<object> action = (x) => Debug.Print("Error: " + x);}
for(;i<50;+i){
Task.Factory.StartNew(action,i);
}