Demystifying async/await

By | 2020-05-07

If you have used async/await, but feel some details are still a bit fuzzy then this may be for you. I thought I’d hop down into the rabbit hole and give some examples of how async/await it actually works. Hopefully this will provide some understanding and get you further on your journey to become an expert.

Async/await is a highly successful feature introduced in C# and copied by other languages. But despite its success and apparent simplicity it comes with complexity. I regularly see people who have not understood it, even to the extent of a whole talk on a developer conference with misconceptions about it.

Async does not create a thread

I see people believe that putting async in front the method spawns off a thread to handle it.

So lets look at a simple code case written in a Console application. We print thread id for each line. In this case we do not await Test() because we want to see if an async method will run on a background thread.

If an async method runs on a separate thread then this should print:
Before: 1
After: 1
Test: 2

It does not, it runs in sequential order on the same thread.

Result
Before: 1
Test: 1
After: 1

Async can be used to make code asynchronous

Let’s modify the code slightly:

Result
Before: 1
After: 1
Test: 2

So all we did was change how we wait, and the method is executed in a background thread. At first glance this may not make much sense. In fact, if we put in a Write() statement before the delay, it would run on thread id 1.

What is happening behind the scenes is that Task.Delay() actually creates a callback timer and then returns the current thread, so the thread goes back and continues to Write("After"...) (and wait for keyboard input). When 1000 ms has passed the operating system triggers the callback and a (randomly picked) free thread is put to work. This thread then continues execution. (At this point you may ask how this is even possible. Can .Net transfer the stack to another thread? We’ll get to that all the way at the end.)

The important takeaway here is that while waiting, our main thread (id 1) was returned immediately upon Task.Delay(), and it could go about its day as if the Test() method had completed.

Note that Task.Dely() is NOT the way to make your code run in the background. Task.Run or Task.Factory.StartNew is the way to go.

Why?

So that brings us to a question I don’t see many ask. Why? 1-2 extra threads are not a big problem, so why are all methods in the .Net Framework being refactored into supporting asynchronous calls?

For high performance scenarios having asynchronous database communication, network communication, disk IO, etc is good. Tests have proven that the maximum throughput of servers increase dramatically with this model.

From a technical perspective we have had the ability to write asynchronous code for a very long time. The problem is that the code for it got messy really fast. The code had to be split into multiple methods to handle start of asynchronous operation, callback for end of asynchronous operation and often callback for continuing with the code after you started this. It often ended up as a complex state machine to do fairly simple things like sending/receiving data. Promises are one approach to making this more uniform, but it didn’t really solve the problem. So for things like disk writes we simply wouldn’t bother.

Understanding Asynchronous JavaScript — Callback, Promise and ...

The real benefit is in ease of use. It elegantly solves a problem that otherwise requires complex coding at relatively low cost to the developer. It also makes multi-threading easier. You can for example spin off a few background tasks, wait for all to complete and continue.

Lets modify our code again:

Result
Before: 1
Test: 2
Test: 4
Test: 3
After: 1
Elapsed time: 1000ms

As we can see here we ran 3 tasks simultaneously in the background, each running on separate threads and sleeping for 1000 ms. Total execution time was only 1000ms. And only when all of them were done did we continue. This would be relatively complex to code manually with threads.

Even if we ran 100 simultaneous tasks and the thread pool only had 10 threads, they would still finish in nearly 1000ms. Because most of the time these tasks are not occupying a thread, so the threads can work on other tasks while they are sleeping.

Imagine a webserver that is receiving data from clients over network, writing to log-file, retrieving data from SQL and sending data back to client. Most of the time is spent waiting for network or file IO. Using asynchronous programming the threads are freed up to handle other requests in-between.

All threads are equal

Back to the previous code, lets modify it again. This time we await the Test() call. (You should always await or pull result from a Task.)

Result
Before: 1
Test: 2
After: 2

So we can see that the new thread is continuing execution for us. This explains why you can’t use await in the body of a lock-statement. But it also demonstrates that with async/await your threads are playing tag-team with each other.

GUI threads are special

When you work with almost any GUI framework (like WPF or WinForms) they are usually not thread safe. This means that only one thread can access it at any time. Usually events from the operating system (mouse move, repaint, click, keypress, etc) are queued by the operating system.

The GUI thread will read the event queue regularly and fire off events or update GUI accordingly. So, in order to achieve thread safety, any work on the GUI must be done on the GUI thread. If not there is no guarantee of thread safety and you will get race conditions.

And to digress even further: If you occupy the GUI thread for too long the event queue will fill up and Windows will gray out the application and tell user that the application is not responding. So there you have it, applications that stop responding would often benefit from async/await.

SynchronizationContext

We have seen that any random thread can continue execution, and GUI applications require you to only modify GUI objects from GUI thread. So how can we use async/await in a GUI application?

The answer lies in the SynchronizationContext. When a GUI application starts, a synchronization context is set up which is used by await to queue continued execution back to the original GUI-thread. This happens transparently and you don’t have to worry too much about it.

Lets run the same code in an WPF application. Remember our output was:
Before: 1
After: 1
Test: 2

I have hooked up the code to a button click event:

Result
Before: 1
Test: 1
After: 1

The same code acts different from when we ran it in a Console application, because in the WPF application a synchronization context has been set up and the code will queue anything after “await” back to the main GUI-thread. It may look like we now lost the benefits of async/await.

What is happening here is that await Task.Delay() is (still) causing the thread to be released, in this case the GUI thread. But after each await, the continuation of code is queued back on the GUI thread (which now is free) with the help of the synchronization context.

ConfigureAwait

You may have seen people or even your editor pointing out that you need to use ConfigureAwait. This is actually where that comes in. By adding .ConfigureAwait(false) to the task you are allowing a random thread to continue the execution.

Lets modify the code to allow the await in Test() to continue on any thread.

Result
Before: 1
Test: 2
After:1

We could further use .ConfigureAwait(false) to await Test(), but that would make the last line in Button_Click method execute under a non-GUI thread and therefore fail. Visual Studio has a Managed Debugging Assistant (MDA) that will detect cross-thread calls to the GUI and stop the application.

Making your own async methods

Being a consumer of async methods is great. But creating your own truly async methods can help simplify the application logic. There are many use cases, and async/await is a good tool to have in your toolbox.

In one of the examples above we used async-await to execute 3 tasks in parallel. As we saw in the first example, the only reason why they actually run in parallel is how we sleep; The fact that we release the thread using Task.Delay() instead of freeze the thread with Thread.Sleep(). But as I mentioned, Task.Delay() is not the correct way for starting work on a different thread. That is just a happy side effect of my example.

Background workers

Lets modify the code to properly run 3 tasks. I am using Thread.Sleep() now to simulate a longer synchronous operation that will not produce our happy side effect.

Again this executes on separate threads in approximately 1000ms. So we have achieved full and synchronized multi-threading using minimum of code. I put the tasks into an array (someTasks) just to demonstrate that you can do other stuff while they work on their stuff.

Waiting for event

Another use case is where you want to pause and wait for something. There are several options here, one is TaskCompletionSource. It will release your thread while waiting for the result to be set.

In this example I click button 1, then click button 2. The code inside button 1 pauses until I click button 2, but everything is happening on the same thread. Neato!

Result
Before: 1
Button 2 clicked: 1
After: 1
Setting button to: testing…

Is it wrong to just .Wait()?

You can make an async method work like a synchronous method by .Wait() or .Result. It does negate the effect of async-await. But code is not perfect, you are allowed to be pragmatic about it. No need to refactor your whole application because into async because you have call an async method somewhere.

Just be aware that you will now occupy two threads. One is waiting for the duration, the other activates when needed inside the async method. Remember that your ThreadPool is a limited resource.

But be careful: If you do this in your GUI application this means that you GUI will freeze. It will not be able to read and act upon the event queue. That is not good for user experience, and can lead to “Application not responding” message. Also if the async method is using Dispatcher for GUI execution you may end up in a permanent deadlock, since the GUI thread is waiting for the async task which is waiting for the GUI thread.

Do I have to return await?

No.

Methods of Task void returns immediately?

They behave as normal async Task methods. If they do not contain code with async-await, they will run as normal synchronous methods. If they contain async-await they will return when the thread is released (as in our Task.Delay() example). The task will continue to run in the background on a separate thread as expected. This is however not good because you will not catch any exceptions from execution. So generally the recommendation is to avoid return type Task void on methods.

Do I always have to await, Wait(), .Result or similar?

No, you do not.

But then you will not know if an exception happened on the task. Any exception in the task that is not handled will be stored in the task object, waiting for you to access it. When you wait for it/pull the result the exception is thrown or made available to you. If you do not check them then you will never know.

So you can say it’s the same as using try { code(); } catch {} to ignore errors, an anti-pattern that sadly I’ve seen a bit too often. Always joy to find one of those after an hour of debugging.

Will Garbage Collect take my Task if I don’t keep a reference?

References to tasks are kept internally in the TPL system, so you do not have to keep a reference to keep your task alive. You can fire and forget Task.Run.

Ok, so how does this magic work?

State machine!

Your async method is split into multiple parts and put into a state machine object. Every “await” you use will split the method.

This state machine exposes MoveNext() method internally that can continue the next step of execution. It also catches exceptions and hides them until later when you check the task, or bubbles them up the stack directly if you await the task.

There is more to it, but this should give you an overall idea of what is happening under the hood.

Here is an example of our Main method:

The compiler turns it into something like this:

Wait, Task is an object? Thats bad, right?

Yes and no. Yes objects are “bad” due to overhead and memory location. Methods may be executed very often, which can have some performance cost. This is somewhat remedied by caching and reuse of Task objects. But there is a better solution: ValueTask.

As a structure it does not suffer the same overhead as an object. Simply return ValueTask instead of Task.

Leave a Reply

Your email address will not be published. Required fields are marked *