Question

I am writing a multi player game server and am looking at ways the new C# async/await features can help me. The core of the server is a loop which updates all the actors in the game as fast as it can:

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

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

This loop is required to handle thousands of actors and update multiple times per second to keep the game running smoothly. Some actors occasionally perform slow tasks in their update functions, such as fetching data from a database, which is where I'd like to use async. Once this data is retrieved the actor wants to update the game state, which must be done on the main thread.

As this is a console application, I plan to write a SynchronizationContext which can dispatch pending delegates to the main loop. This allows those tasks to update the game once they complete and lets unhandled exceptions be thrown into the main loop. My question is, how do write the async update functions? This works very nicely, but breaks the recommendations not to use 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;
}

I could make Update() async and propogate the tasks back to the main loop, but:

  • I don't want to add overhead to the thousands of updates that will never use it.
  • Even in the main loop I don't want to await the tasks and block the loop.
  • Awaiting the task would cause a deadlock anyway as it needs to complete on the awaiting thread.

What do I do with all these tasks I can't await? The only time I might want to know they've all finished is when I'm shutting the server down, but I don't want to collect every task generated by potentially weeks worth of updates.

Was it helpful?

Solution

My understanding is that the crux of it is that you want:

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
   ...
}

Where the while loop is running in your main console thread. This is a tight single-threaded loop. You could run the foreach in parallel, but then you would still be waiting for the longest running instance (the i/o bound operation to get the data from the database).

await async is not the best option within this loop, you need to run these i/o database tasks on a thread pool. On the thread pool async await would be useful to free up pool threads.

So, the next question is how to get these completions back to your main thread. Well, it seems like you need something equivalent to a message pump on your main thread. See this post for information on how to do that, though that may be a bit heavy handed. You could just have a completion queue of sorts that you check on the main thread in each pass through your while Loop. You would use one of the concurrent data structures to do this so that it is all thread safe then set Foo if it needs to be set.

It seems that there is some room to rationalise this polling of actors and threading, but without knowing the details of the app it is hard to say.

A couple of points: -

  • If you do not have a Wait higher up on a task, your main console thread will exit and so will your application. See here for details.

  • As you have pointed out, await async does not block the current thread, but it does mean that the code subsequent to the await will only execute on completion of the await.

  • The completion may or may not be completed on the calling thread. You have already mentioned Synchronization Context, so I won't go into the details.

  • Synchronization Context is null on a Console app. See here for information.

  • Async isn't really for fire-and-forget type operations.

For fire and forget you can use one of these options depending on your scenario:

  • Use Task.Run or Task.StartNew. See here for differences.
  • Use a producer/consumer type pattern for the long running scenarios running under your own threadpool.

Be aware of the following: -

  • That you will need to handle the exceptions in your spawned tasks / threads. If there are any exceptions that you do not observe, you may want to handle these, even just to log their occurence. See the information on unobserved exceptions.
  • If your process dies while these long running tasks are on the queue or starting they will not be run, so you may want some kind of persistence mechanism (database, external queue, file) that keeps track of the state of these operations.

If you want to know about the state of these tasks, then you will need to keep track of them in some way, whether it is an in memory list, or by querying the queues for your own thread pool or by querying the persistence mechanism. The nice thing about the persistence mechanism is that it is resilient to crashes and during shutdown you could just close down immediately, then pick up where you ended up when you restart (this of course depends on how critical it is that the tasks are run within a certain timeframe).

OTHER TIPS

First, I recommend that you do not use your own SynchronizationContext; I have one available as part of my AsyncEx library that I commonly use for Console apps.

As far as your update methods go, they should return Task. My AsyncEx library has a number of "task constants" that are useful when you have a method that might be asynchronous:

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

Returning to your main loop, the solution there isn't quite as clear. If you want every actor to complete before continuing to the next actor, then you can do this:

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

Alternatively, if you want to start all actors simultaneously and wait for them all to complete before moving to the next "tick", you can do this:

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

When I say "simultaneously" above, it is actually starting each actor in order, and since they all execute on the main thread (including the async continuations), there's no actual simultaneous behavior; each "chuck of code" will execute on the same thread.

I highly recommend watching this video or just taking a look at the slides: Three Essential Tips for Using Async in Microsoft Visual C# and Visual Basic

From my understanding what you should probably be doing in this scenario is returning Task<Thing> in UpdateAsync and possibly even Update.

If you are performing some async operations with 'foo' outside the main loop what happens when the async part completes during a future sequential update? I believe you really want to wait on all your update tasks to complete and then swap your internal state over in one go.

Ideally you would start all the slow (database) updates first and then do the other faster ones so that the entire set is ready as soon as possible.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top