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

标签 c# asynchronous task

我一直在玩弄并行性,但我在理解我的程序中发生的事情时遇到了一些困难。

我正在尝试复制 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);
}

这会引发一个奇怪的错误:

An unhandled exception of type 'System.AggregateException' occurred in mscorlib.dll

内部异常谈论索引超出范围。

如果我改变

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

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

那么这似乎可行(或者至少程序无一异常(exception)地执行),但数组中没有足够的空间容纳所有组件。尽管如此,所有组件都已更新,尽管 tasks 中没有足够的空间。数组来容纳它们。

然而,gameTime作为参数传递的对象在游戏运行时变得有些疯狂。我发现很难确定问题所在,但我有两个组件,它们都使用

简单地移动圆的 x 位置
x += (float)(gameTime.ElapsedGameTime.TotalSeconds * 10);

但是,当使用 Tasks 时,它们的 x 位置很快就会变得完全不同,而实际上它们应该是相同的。每个engineComponent.Update(gameTime)每个更新周期调用一次,相同的 gameTime传递对象。

使用 tasks[i].RunSynchronously(); 时代替 tasks[i].Start(); ,程序完全按预期运行。

我知道以这种方式使用任务可能不是一种特别有效的编程实践,所以我的问题是出于好奇:为什么上面的代码没有像我预期的那样工作?我知道我遗漏了一些明显的东西,但我一直无法查明此实现的具体问题。

很抱歉问了这么长的问题,感谢阅读;)

最佳答案

问题是您的 lambda 表达式正在捕获 i - 不是 ivalue,而是变量本身。

这意味着当您的任务执行时,循环很可能在下一次迭代(甚至更晚)。所以你的一些组件可能更新不止一次,有些可能根本就没有更新,最后的任务很可能在 i 超出 engineComponents 的范围时执行,因此异常(exception)。有关详细信息,请参阅 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 迭代变量的行为是将其作为单个变量,就像。您不需要以 .NET 4.5 或更高版本为目标,但您需要使用 C# 5 编译器。

另一种选择是完全显式不使用任务——使用Parallel.ForEach相反:

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

简单多了!

关于C# 任务未按预期工作。奇怪的错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20853831/

相关文章:

c# - 将 DataGridView CheckBox Column 用于字符串数据列

javascript - 基于先前调用的响应的异步调用,避免回调 hell

c# - 帮助消除由于 HTTP 响应中的 content-length = 0 而导致的异步接收器中的循环

c# - 多线程中的入队和出队

c# - Task 和 void 方法之间的区别(非异步)

c# - Linux VPS 上的单声道垃圾收集

c# - 如何在 C# 中使用 datetimepicker 将日期(长格式)插入到 Access 数据库中? (错误仅在日期部分)

c# - 如何调试windows store app OnActivated事件

javascript - 如何简化tamejs中的错误处理?

multithreading - JavaFX - 取消任务不起作用