11

我正在编写一个多人游戏服务器,并且正在寻找新的 C# async/await 功能可以帮助我的方式。服务器的核心是一个循环,它会尽可能快地更新游戏中的所有参与者:

while (!shutdown)
{
    foreach (var actor in actors)
        actor.Update();

    // Send and receive pending network messages
    // Various other system maintenance
}

这个循环需要处理数千个actor并每秒更新多次以保持游戏流畅运行。有些演员偶尔会在他们的更新功能中执行缓慢的任务,例如从数据库中获取数据,这是我想使用异步的地方。一旦检索到这些数据,actor 想要更新游戏状态,这必须在主线程上完成。

由于这是一个控制台应用程序,我计划编写一个 SynchronizationContext,它可以将挂起的委托分派到主循环。这允许这些任务在完成后更新游戏,并将未处理的异常抛出到主循环中。我的问题是,如何编写异步更新函数?这非常有效,但违反了不使用 async void 的建议:

Thing foo;

public override void Update()
{
    foo.DoThings();

    if (someCondition) {
        UpdateAsync();
    }
}

async void UpdateAsync()
{
    // Get data, but let the server continue in the mean time
    var newFoo = await GetFooFromDatabase();

    // Now back on the main thread, update game state
    this.foo = newFoo;
}

我可以使 Update() 异步并将任务传播回主循环,但是:

  • 我不想为永远不会使用它的数千个更新增加开销。
  • 即使在主循环中,我也不想等待任务并阻塞循环。
  • 等待任务无论如何都会导致死锁,因为它需要在等待线程上完成。

我该如何处理所有这些我无法等待的任务?我可能想知道他们都完成的唯一一次是当我关闭服务器时,但我不想收集可能需要数周的更新产生的每一个任务。

4

3 回答 3

9

我的理解是,关键是你想要:

while (!shutdown)
{
    //This should happen immediately and completions occur on the main thread.
    foreach (var actor in actors)
        actor.Update(); //includes i/o bound database operations

    // The subsequent code should not be delayed
   ...
}

while 循环在主控制台线程中运行的位置。这是一个紧密的单线程循环。您可以并行运行 foreach,但您仍将等待运行时间最长的实例(从数据库获取数据的 i/o 绑定操作)。

await async 不是此循环中的最佳选择,您需要在线程池上运行这些 i/o 数据库任务。在线程池上,异步等待对于释放池线程很有用。

因此,下一个问题是如何将这些完成返回到您的主线程。好吧,您似乎需要在主线程上使用类似于消息泵的东西。有关如何做到这一点的信息,请参阅这篇文章,尽管这可能有点笨拙。您可以有一个完成队列,您可以在每次通过 while 循环时检查主线程。您将使用其中一种并发数据结构来执行此操作,以便它是所有线程安全的,然后在需要设置时设置 Foo 。

似乎有一些空间可以合理化这种对参与者和线程的轮询,但在不了解应用程序的细节的情况下很难说。

几点:-

  • 如果您没有等待更高的任务,您的主控制台线程将退出,您的应用程序也将退出。有关详细信息,请参见此处

  • 正如您所指出的,await async 不会阻塞当前线程,但这确实意味着 await 之后的代码只会在 await 完成时执行。

  • 完成可能会或可能不会在调用线程上完成。您已经提到了同步上下文,因此我不再赘述。

  • 控制台应用程序上的同步上下文为空。有关信息,请参见此处

  • Async 并不是真正用于即发即弃类型的操作。

对于“一劳永逸”,您可以根据您的情况使用以下选项之一:

  • 使用 Task.Run 或 Task.StartNew。请参阅此处了解差异
  • 对于在您自己的线程池下运行的长时间运行的场景,请使用生产者/消费者类型模式。

请注意以下事项: -

  • 您将需要处理生成的任务/线程中的异常。如果有任何你没有观察到的异常,你可能想要处理这些,甚至只是记录它们的发生。请参阅有关未观察到的异常的信息。
  • 如果您的进程在这些长时间运行的任务在队列中或启动时死亡,它们将不会运行,因此您可能需要某种持久性机制(数据库、外部队列、文件)来跟踪这些操作的状态。

如果您想了解这些任务的状态,那么您将需要以某种方式跟踪它们,无论是内存列表,还是通过查询队列以获取您自己的线程池或通过查询持久性机制。持久性机制的好处是它对崩溃具有弹性,并且在关闭期间您可以立即关闭,然后在重新启动时选择您最终的位置(这当然取决于任务在其中运行的重要性一定的时间范围)。

于 2013-09-10T03:31:02.823 回答
3

首先,我建议你不要使用自己的SynchronizationContext;我有一个作为我的AsyncEx 库的一部分,我通常用于控制台应用程序。

就您的更新方法而言,它们应该返回Task. 我的 AsyncEx 库有许多“任务常量”,当你有一个可能是异步的方法时它们很有用:

public override Task Update() // Note: not "async"
{
  foo.DoThings();

  if (someCondition) {
    return UpdateAsync();
  }
  else {
    return TaskConstants.Completed;
  }
}

async Task UpdateAsync()
{
  // Get data, but let the server continue in the mean time
  var newFoo = await GetFooFromDatabase();

  // Now back on the main thread, update game state
  this.foo = newFoo;
}

回到你的主循环,那里的解决方案还不是很清楚。如果您希望每个演员在继续下一个演员之前完成,那么您可以这样做:

AsyncContext.Run(async () =>
{
  while (!shutdown)
  {
    foreach (var actor in actors)
      await actor.Update();
    ...
  }
});

或者,如果您想同时启动所有参与者并等待它们全部完成,然后再移动到下一个“tick”,您可以这样做:

AsyncContext.Run(async () =>
{
  while (!shutdown)
  {
    await Task.WhenAll(actors.Select(actor => actor.Update()));
    ...
  }
});

当我在上面说“同时”时,它实际上是按顺序启动每个参与者,并且由于它们都在主线程上执行(包括async延续),因此没有实际的同时行为;每个“代码块”都将在同一个线程上执行。

于 2013-09-10T13:07:20.520 回答
0

我强烈建议观看此视频或只是看看幻灯片: 在 Microsoft Visual C# 和 Visual Basic 中使用异步的三个基本技巧

据我了解,在这种情况下您可能应该做的是返回Task<Thing>UpdateAsync甚至可能是Update.

如果您在主循环之外使用“foo”执行一些异步操作,那么在未来的顺序更新期间异步部分完成时会发生什么?我相信您真的想等待所有更新任务完成,然后一次性交换您的内部状态。

理想情况下,您将首先启动所有慢速(数据库)更新,然后再进行其他较快的更新,以便整个集合尽快准备好。

于 2013-09-10T06:12:35.257 回答