C#任务未按预期工作。奇怪的错误。

5
我一直在尝试并行计算,但是我对程序中发生的事情感到困惑。
我正在尝试复制XNA框架的一些功能。 我使用了组件样式的设置,并且我想通过在单独的任务中调用每个组件的Update方法来使我的程序更加高效。 然而,显然我做错了什么。
我在循环中更新调用的代码如下:
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);
}

这个出现了奇怪的错误:

一个未经处理的类型为 'System.AggregateException' 的异常在 mscorlib.dll 中发生

内部异常提到了索引超出范围。

如果我改变

Task[] tasks = new Task[engineComponents.Count];

to

Task[] tasks = new Task[engineComponents.Count - 1];

如果这样做的话似乎是有效的(或者至少程序不会抛出异常),但是数组中没有足够的空间来存储所有组件。尽管如此,所有组件都会被更新,尽管 tasks 数组的空间不足以容纳它们。

然而,在游戏运行时作为参数传递的 gameTime 对象有些失控。我很难确定问题所在,但是我有两个组件都仅使用一个圆形的x位置进行移动。

x += (float)(gameTime.ElapsedGameTime.TotalSeconds * 10);

然而,当使用“Tasks”时,它们的x位置很快就会相互分离,但实际上它们应该是相同的。每个“engineComponent.Update(gameTime)”在每个更新周期中调用一次,并传递相同的“gameTime”对象。
在使用“tasks[i].RunSynchronously()”替换“tasks[i].Start()”时,程序按预期运行。
我知道以这种方式使用任务可能不是特别有效的编程实践,所以我的问题是出于好奇:为什么上述代码没有按照预期工作?我知道我错过了一些显而易见的东西,但我一直无法追踪具体哪里出了问题。
对于这个长问题,我表示歉意,感谢您的阅读;)
3个回答

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
那里的解释根本不清楚—— i确实被捕获了,但捕获的是变量而不是值。 Lambda表达式永远不会捕获一个值,而总是捕获一个变量。这并不像 OP 可以“记住”去捕获 i 值一样。 - Jon Skeet

8
问题在于你的lambda表达式捕获了变量i - 不是i的值,而是变量本身。
这意味着在任务执行时,循环可能已经进入下一次迭代(甚至更晚)。因此,你的一些组件可能会被更新多次,有些组件可能根本没有被更新,最终任务可能会在i超出engineComponents范围时执行,从而导致异常。有关更多详细信息,请参见Eric Lippert的博客文章: 修复此问题的三个选项:
  • Take a copy of the variable inside the loop. Each variable declared inside the loop will be captured separately:

    for (int i = 0; i < tasks.Length; i++)
    {
        int copyOfI = i;
        tasks[i] = new Task(() => engineComponents[copyOfI].Update(gameTime));
        tasks[i].Start();
    }
    
  • Capture engineComponents[i] in a separate variable instead:

    for (int i = 0; i < tasks.Length; i++)
    {
        var component = engineComponents[i];
        tasks[i] = new Task(() => component.Update(gameTime));
        tasks[i].Start();
    }
    
  • If you're using C# 5, using a foreach loop will do what you want:

    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迭代变量的行为是它是一个单独的变量,就像i一样。您不需要针对.NET 4.5或更高版本进行操作即可使其工作,但您需要使用C# 5编译器。

另一种选择是根本明确使用任务-改用Parallel.ForEach

// This replaces your entire method body
Parallel.ForEach(engineComponents, component => component.Update(gameTime));

简单多了!


2

您的循环变量被封闭在闭包中。这里有一篇由Eric Lippert撰写的文章,可以提供更详细的解释。您可以通过在循环内部声明一个内部变量来轻松解决此问题:

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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接