C# TaskFactory 未完成所有迭代
C# TaskFactory not completing all iterations
我在使用 TaskFactory 时遇到了一些问题:
Task<int[]> parent = Task.Run(() =>
{
int length = 100;
var results = new int[length];
TaskFactory tf = new TaskFactory(TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously);
// Create a list of tasks that we can wait for
List<Task> taskList = new List<Task>();
for (int i = 0; i < length - 1; i++) // have to set -1 on length else out of bounds exception, huh?
{
taskList.Add(tf.StartNew(() => results[i] = i));
}
// Now wait for all tasks to complete
Task.WaitAll(taskList.ToArray());
return results;
});
parent.Wait();
var finalTask = parent.ContinueWith(
parentTask =>
{
foreach (int i in parentTask.Result)
{
Console.WriteLine(i);
}
});
finalTask.Wait();
Console.ReadKey();
这给出了类似于以下的输出:
0
0
0
0
4个
5个
0
0
0
0
10
0
12
13
14
...
0
99
我不明白为什么不是所有索引都是非零的。
谢谢,
乔
当您使用 lambda 捕获变量时,该变量将被放入编译器生成的对象中,该对象在内部范围和外部范围之间共享。当你这样做时:
for (int i = 0; i < length - 1; i++)
{
taskList.Add(tf.StartNew(() => results[i] = i));
}
变量 i
在循环和所有子任务之间共享。只有一个 i
,它正在被循环修改,因为任务是 运行ning。这是一个竞争条件,每次都会在数组中产生看似随机的数据,具体取决于任务的安排方式。
解决这个问题的最简单方法是创建一个作用域为循环体的不可变变量:
for (int i = 0; i < length; i++) // You can now use length instead of length - 1
{
int innerI = i;
taskList.Add(tf.StartNew(() => results[innerI] = innerI));
}
现在每个任务都有一个单独的 innerI
变量,它被赋值 i
恰好一次并且不会改变。
想象一下编译器将旧代码转换成
class Generated1
{
public int i;
}
var context = new Generated1(); // Exactly one copy of i
for (context.i = 0; context.i < length - 1; context.i++)
{
taskList.Add(tf.StartNew(() => results[context.i] = context.i));
}
而编译器转换后的新代码变为
class Generated2
{
public int innerI;
}
for (int i = 0; i < length - 1; i++)
{
var context = new Generated2(); // New copy of innerI for every loop iteration
context.innerI = i;
taskList.Add(tf.StartNew(() => results[context.innerI] = context.innerI));
}
关于为什么你必须使用 length - 1
:你的一些任务在循环完成之前没有 运行。那时,i == length
,因此当您尝试使用 i
作为数组的索引时,您会得到一个 IndexOutOfRangeException
。当您使用 i
修复竞争条件时,这种情况就不会再发生了。
我在使用 TaskFactory 时遇到了一些问题:
Task<int[]> parent = Task.Run(() =>
{
int length = 100;
var results = new int[length];
TaskFactory tf = new TaskFactory(TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously);
// Create a list of tasks that we can wait for
List<Task> taskList = new List<Task>();
for (int i = 0; i < length - 1; i++) // have to set -1 on length else out of bounds exception, huh?
{
taskList.Add(tf.StartNew(() => results[i] = i));
}
// Now wait for all tasks to complete
Task.WaitAll(taskList.ToArray());
return results;
});
parent.Wait();
var finalTask = parent.ContinueWith(
parentTask =>
{
foreach (int i in parentTask.Result)
{
Console.WriteLine(i);
}
});
finalTask.Wait();
Console.ReadKey();
这给出了类似于以下的输出:
0 0 0 0 4个 5个 0 0 0 0 10 0 12 13 14 ... 0 99
我不明白为什么不是所有索引都是非零的。
谢谢,
乔
当您使用 lambda 捕获变量时,该变量将被放入编译器生成的对象中,该对象在内部范围和外部范围之间共享。当你这样做时:
for (int i = 0; i < length - 1; i++)
{
taskList.Add(tf.StartNew(() => results[i] = i));
}
变量 i
在循环和所有子任务之间共享。只有一个 i
,它正在被循环修改,因为任务是 运行ning。这是一个竞争条件,每次都会在数组中产生看似随机的数据,具体取决于任务的安排方式。
解决这个问题的最简单方法是创建一个作用域为循环体的不可变变量:
for (int i = 0; i < length; i++) // You can now use length instead of length - 1
{
int innerI = i;
taskList.Add(tf.StartNew(() => results[innerI] = innerI));
}
现在每个任务都有一个单独的 innerI
变量,它被赋值 i
恰好一次并且不会改变。
想象一下编译器将旧代码转换成
class Generated1
{
public int i;
}
var context = new Generated1(); // Exactly one copy of i
for (context.i = 0; context.i < length - 1; context.i++)
{
taskList.Add(tf.StartNew(() => results[context.i] = context.i));
}
而编译器转换后的新代码变为
class Generated2
{
public int innerI;
}
for (int i = 0; i < length - 1; i++)
{
var context = new Generated2(); // New copy of innerI for every loop iteration
context.innerI = i;
taskList.Add(tf.StartNew(() => results[context.innerI] = context.innerI));
}
关于为什么你必须使用 length - 1
:你的一些任务在循环完成之前没有 运行。那时,i == length
,因此当您尝试使用 i
作为数组的索引时,您会得到一个 IndexOutOfRangeException
。当您使用 i
修复竞争条件时,这种情况就不会再发生了。