Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 412
Generic Error Avatar asked Sep 10 '13 03:09

Generic Error


People also ask

What happens if you don't await async?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Is it mandatory to use await with async?

short answer no. longer answer also no. await waits for async to resolve or reject. if you dont need the results, you wont need to await.

Can we call async method without await?

In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously. Code after each await expression can be thought of as existing in a .then callback.

Can async method have multiple awaits?

For more information, I have an async / await intro on my blog. So additionally, if a method with multiple awaits is called by a caller, the responsibility for finishing every statement of that method is with the caller.


1 Answers

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

like image 143
acarlon Avatar answered Oct 05 '22 00:10

acarlon