我一直在玩弄并行,我在理解程序中发生的事情时遇到了一些麻烦。
我正在尝试复制XNA框架的一些功能。我正在使用组件式设置,我想让我的程序更高效的一种方法是在单独的Update
中调用每个组件的Task
方法。但是,我显然做了一些可怕的错误。
我在循环中用于更新调用的代码是:
public void Update(GameTime gameTime)
{
Task[] tasks = new Task[engineComponents.Count];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = new Task(() => engineComponents[i].Update(gameTime));
tasks[i].Start();
}
Task.WaitAll(tasks);
}
这引发了一个奇怪的错误:
mscorlib.dll中出现未处理的“System.AggregateException”类型异常
内部异常谈到索引超出范围。
如果我改变
Task[] tasks = new Task[engineComponents.Count];
到
Task[] tasks = new Task[engineComponents.Count - 1];
然后这似乎工作(或者至少程序执行没有异常),但是数组中没有足够的空间用于所有组件。尽管如此,所有组件都会更新,尽管tasks
阵列中没有足够的空间来容纳它们。
但是,当游戏运行时,作为参数传递的gameTime
对象有点疯狂。我发现很难确定这个问题,但我有两个组件,它们都只是使用
x += (float)(gameTime.ElapsedGameTime.TotalSeconds * 10);
然而,当使用Tasks
时,他们的x位置很快会彼此完全不同,而实际上它们应该是相同的。每个更新周期调用一次engineComponent.Update(gameTime)
,并传递相同的gameTime
对象。
使用tasks[i].RunSynchronously();
代替tasks[i].Start();
时,程序会按预期运行。
据我所知,以这种方式使用Tasks可能不是一种特别有效的编程习惯,所以我的问题是好奇心:为什么上述代码不像我期望的那样工作?我知道我遗漏了一些明显的东西,但是我无法追查这个实现的具体问题。
为长期问题道歉,并感谢阅读;)
答案 0 :(得分:8)
尝试以下方法:
for (int i = 0; i < tasks.Length; i++)
{
var innerI = i;
tasks[i] = new Task(() => engineComponents[innerI].Update(gameTime));
tasks[i].Start();
}
您需要为每个任务创建一个新变量,该变量将由linq表达式捕获并保存作业部分的索引。现在,您的所有任务都使用i
变量并对最新元素执行操作。
答案 1 :(得分:8)
问题是你的lambda表达式正在捕获i
- 而不是i
的值,而是变量本身。
这意味着,当您的任务执行时,循环可能会在下一次迭代(甚至更晚)。因此,您的某些组件可能会多次更新,某些组件可能根本不会更新,并且当i
超出engineComponents
范围时,最终任务可能会执行,因此异常。有关更多详细信息,请参阅Eric Lippert的博客文章:
解决此问题的三个选项:
在循环中获取变量的副本。在循环内声明的每个变量将被单独捕获:
for (int i = 0; i < tasks.Length; i++)
{
int copyOfI = i;
tasks[i] = new Task(() => engineComponents[copyOfI].Update(gameTime));
tasks[i].Start();
}
在一个单独的变量中捕获engineComponents[i]
:
for (int i = 0; i < tasks.Length; i++)
{
var component = engineComponents[i];
tasks[i] = new Task(() => component.Update(gameTime));
tasks[i].Start();
}
如果您使用的是C#5,使用foreach
循环将执行您想要的操作:
var tasks = new List<Task>();
foreach (var component in engineComponents)
{
Task task = new Task(() => component.Update(gameTime));
tasks.Add(task);
task.Start();
}
Task.WaitAll(tasks.ToArray());
请注意,最后一个解决方案将不与C#4编译器一起工作,因为foreach
迭代变量的行为是它是一个单一变量,就像{{1 }}。您不需要以.NET 4.5或更高版本为目标,但它确实需要使用C#5编译器。
另一个选择是根本不使用明确任务 - 改为使用Parallel.ForEach
:
i
更简单!
答案 2 :(得分:2)
您的循环变量在闭包中捕获。以下是Eric Lippert的article更详细的解释。您可以通过在循环内声明内部变量来轻松解决问题:
public void Update(GameTime gameTime)
{
Task[] tasks = new Task[engineComponents.Count];
for (int i = 0; i < tasks.Length; i++)
{
int inner = i; // Declare another temp variable
tasks[i] = new Task(() => engineComponents[inner].Update(gameTime));
tasks[i].Start();
}
Task.WaitAll(tasks);
}