What do I do with async Tasks I don't want to wait for?
Asked Answered
N

3

11

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.

Norther answered 10/9, 2013 at 3:1 Comment(6)
An interesting question, +1 and added [task-parallel-library] tag.Sharkskin
Once this data is retrieved the actor wants to update the game state, which must be done on the main thread. Is the game state update itself a sync or async operation?Sharkskin
Sync, and should be done in serial. This is generally a fast single threaded loop, with occasional background tasks which need to be spawned and rejoined.Norther
Check the 2nd listing in my answer, I think it does just that for game state updates using queued callbacks. For actor updates, you could switch from Parallel.ForEach to regular foreach if you want them to be executed in serial too, but you would pretty much loose the benefits of multicore CPU architecture.Sharkskin
Sorry to necro this post but.... anyone trying to use async in a real-time game should probably adopt the swapbuffers paradigm from OpenGL (that's also commonly used in such games). You need one instance of game state that's "how things were" and can no longer be changed. A bunch of actors can read from that in parallel, decide what they will do, and queue changes that are made to the "things are becoming" copy of the game state. You need to await all of the changes in order to have a new consistent state, that swaps into the new "how things are".Natale
Each time a game state is promoted to "how things are", you can allow it to be used not only for starting to compute actions in the next timestep, but also rendering, quicksaving, sending across the network, and so on.Natale
C
9

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

Camouflage answered 10/9, 2013 at 3:31 Comment(9)
So would I do something like, Task.Run to start the task, then have a method I can call to queue a delegate for the main loop to run? It feels a bit like I'm building my own very simple version of the async functionality.Norther
I have updated my post with information about await async. It does not actually block the thread.Camouflage
No, however if I understand it correctly it will cause the rest of the current method to be set up as a continuation for when the awaited task completes. This means await is not suitable in the main loop which must carry on immediately.Norther
I have updated my response to indicate that you were already aware of that. Correct, the code after the await will only occur after the await has completed. If this is unacceptable, you will need to restructure the code or launch the code on a new thread. The async approach would work if the subsequent code only needed to run when the async completes, but in your case it needs to be immediate.Camouflage
Correction: "launch the code on a new thread" should be "do the update on a new thread/thread pool"Camouflage
Thanks for all the thoughts. I'm going to try writing a prototype which makes a couple of helpers available to Update methods that want them: a) Start a task on the thread pool, with my standard logging etc attached. b) Schedule a delegate to run later in the main loop.Norther
No problem. It is a good question. I have done a further update.Camouflage
let us continue this discussion in chatNorther
I would like to, but have to head out, will check back in about two hours.Camouflage
R
3

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.

Recursion answered 10/9, 2013 at 13:7 Comment(2)
Hi Stephen, I have spent quite some time going over your blog and projects, thanks for all of your work. This case is perhaps slightly odd in that I do not want the loop to await the tasks, in fact we might go through the whole main loop and call Update on an actor multiple times before its background task completes. Perhaps I'm confusing the purpose of async, as what I'm really doing is starting another task which is sort of unrelated to the Update itself.Norther
Well, you can trigger each update without awaiting the result, but this brings up questions around error handling. Sooner or later you'll want to "sync" back up and propagate exceptions.Recursion
B
0

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.

Blueweed answered 10/9, 2013 at 6:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.