我对线程安全方法参数的理解是:按值传递到方法的参数中的参数作为方法调用的参数中给出的数据的副本传递,因此它们对于该方法调用是唯一的,并且不能由任何其他任务更改。相反,引用参数容易受到其他任务中运行的代码的更改。
话虽如此,我并不完全清楚为什么以下代码(不制作循环计数器的本地副本(在每个线程中返回相同的数字。
static void ExampleFunc(int i) =>
Console.WriteLine("task " + i);
for (int i = 0; i < 10; i++)
{
int taskN = i; //local copy instead of i
Task.Run(() => Func(i));
}
实际输出为:任务10十次
我通过传递任务N而不是i来获得正确的输出(任务1到10(。
我期望相同的结果,因为我正在传递一个类型值参数。
按值传递到方法的参数作为方法调用的参数中给出的数据的副本传递,
问题实际上是:这种复制何时发生?
这不是当你Task.Run(...);
的时候;而是当线程池调用实际的lambda时,即当Func(i)
被执行时。这里的问题是,在大多数情况下,线程池会比活动线程上的循环慢,所以这些都将在循环完成后发生,并且它们都将访问相同的捕获值 i
。最终,您拥有的是:
class CaptureContext {
public int i;
public void Anonymous() { Func(i); }
}
...
var ctx = new CaptureContext();
for (ctx.i = 0; ctx.i < 10; ctx.i++)
{
int taskN = ctx.i; // not used, so will probably be removed
Task.Run(ctx.Anonymous);
}
即只有一个i
,所以如果所有匿名方法在循环后被调用,则所有方法的值将为:10
。
将代码更改为:
int taskN = i; //local copy instead of i
Task.Run(() => Func(taskN));
给你非常不同的语义:
class CaptureContext {
public int taskN;
public void Anonymous() { Func(taskN);}
}
...
for (int i = 0 ; i < 10 ; i++)
{
var ctx = new CaptureContext();
ctx.taskN = i;
Task.Run(ctx.Anonymous);
}
请注意,我们现在有 10 个捕获上下文实例,每个实例都有自己的 taskN
值,每个上下文都是唯一的。