我正在编写一个多人游戏服务器,我正在研究新的C#async / await功能的方法 帮我。服务器的核心是一个循环,它可以快速更新游戏中的所有角色 可以:
while (!shutdown)
{
foreach (var actor in actors)
actor.Update();
// Send and receive pending network messages
// Various other system maintenance
}
这个循环需要处理数千个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()异步并将任务传播回主循环,但是:
如何处理我无法等待的所有这些任务?我唯一一次想知道他们都是 完成是我关闭服务器,但我不想收集生成的每个任务 可能需要数周的更新。
答案 0 :(得分: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绑定操作)。
等待异步不是此循环中的最佳选项,您需要在线程池上运行这些i / o数据库任务。在线程池上,async await对于释放池线程非常有用。
因此,接下来的问题是如何将这些完成返回到主线程。好吧,看起来你需要一些与主线程上的消息泵相当的东西。有关如何执行此操作的信息,请参阅this post,尽管这可能有点沉重。您可以只有一个完整队列,您可以通过while循环检查每个传递中的主线程。您可以使用其中一个concurrent data structures来执行此操作,以便它是所有线程安全的,然后在需要设置时设置Foo。
似乎有一些空间来合理化这种对演员和线程的轮询,但是在不知道应用程序的细节的情况下很难说。
有几点: -
如果您没有在某个任务上等待,则主控制台线程将退出,您的应用程序也将退出。有关详细信息,请参阅here。
正如您所指出的那样,等待异步不会阻塞当前线程,但它确实意味着await之后的代码只会在完成await时执行。
在调用线程上完成可能会也可能不会完成。您已经提到了同步上下文,因此我不会详细介绍。
控制台应用上的同步上下文为空。有关信息,请参阅here。
Async并不适用于即发即弃型操作。
对于火灾和遗忘,您可以根据您的情况使用其中一个选项:
请注意以下事项: -
如果您想了解这些任务的状态,那么您需要以某种方式跟踪它们,无论是内存列表,还是通过查询自己的线程池的队列或查询持久性机制。关于持久性机制的好处在于它对崩溃具有弹性,在关机期间你可以立即关闭,然后在重新启动时获取你最终的位置(这当然取决于任务在其中运行的重要性)某个时间段。)
答案 1 :(得分:2)
首先,我建议您不要使用自己的SynchronizationContext
;我有一个可用作AsyncEx library的一部分,我通常用于控制台应用。
就更新方法而言,它们应该返回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();
...
}
});
或者,如果你想同时启动所有演员并等待他们全部完成,然后再转到下一个"勾选",你可以这样做:
AsyncContext.Run(async () =>
{
while (!shutdown)
{
await Task.WhenAll(actors.Select(actor => actor.Update()));
...
}
});
当我说"同时"上面,它实际上按顺序启动每个actor,并且因为它们都在主线程上执行(包括async
延续),所以没有实际的同时行为;每个代码"代码"将在同一个线程上执行。
答案 2 :(得分:0)
我强烈建议您观看此视频或只是查看幻灯片: Three Essential Tips for Using Async in Microsoft Visual C# and Visual Basic
根据我的理解,在这种情况下你可能应该做的是在Task<Thing>
甚至UpdateAsync
中返回Update
。
如果您在主循环外部使用'foo'执行某些异步操作,那么在将来的顺序更新期间异步部分完成时会发生什么?我相信你真的想等待所有的更新任务完成,然后一次性交换你的内部状态。
理想情况下,您首先启动所有慢速(数据库)更新,然后执行其他更快速的更新,以便尽快准备好整个集合。