Understanding .NET Async/Await is Critical

Ron Norman emphasizes the critical importance of understanding the async/await pattern in .NET for writing efficient, non-blocking code. This approach is fundamental for enhancing application performance and scalability by managing asynchronous operations effectively. Mastery of async/await allows developers to handle I/O-bound and CPU-bound operations smoothly, significantly improving user experience and system throughput by avoiding unnecessary thread blocking​​.

Ron Norman

6/13/20233 min read

The async/await pattern could tremendously improve the responsiveness of applications. One of the best advancements in coding for responsiveness, and thread-use optimization since the invention of multi-threading. Microsoft has provided an elegant "promise" non-blocking coding scheme implementation in C#.

Because of its simplicity of implementation, the async/await pattern has been spreading through C# code like wild fire, opening the potential for back firing. The usage and pitfalls must be understood by developers when implementing this pattern, otherwise, the application could get plagued with deadlocks, blocking, and unnecessary degradation in performance.

What async/await is not

To start with, let me put forth what the async/await is not, in order to clear any confusion. Let's clear the following out of the way:

  • The async/await pattern does not create new threads

  • The async/await pattern does not do parallel processing

  • The async/await pattern is not and implementation of multi-threading


Fire and Forget

The async/await pattern is about one thing and one thing only: non-blocking. Think of it as an abstraction of a scheduler, where the calling thread "schedules" the execution of the task with the "system" and returns right away. It is up to the "system" how to actually do the work.

It might as well be the same thread picking up the work at the other end. It could be a thread-pool thread, it could be a hardware controller, another process, etc... The point is async/await does not worry about who is running the work, it just delegates it, leaves a call back "number", and returns right away with an incomplete Task.

What about the synchronization context when the call back happens?
In some cases, like a GUI or ASP.NET/Web API, calling back or returning to the same context is critical. For example, a Windows GUI has one main thread in the message pump that is the only one that can update the UI components.

How does async/await ensure that the call back happens on the same context? Before scheduling the execution of work, await captures the current context before returning. Then, when the async work is done and the call back happens, that saved context gets recaptured and the next line of code is executed within that recaptured context.

Potential for deadlocks

Continuing from the example above, when the async work is done and the call back occurs, at the context recapture step: what if that saved context could not be recaptured? You might be thinking how is that possible? This brings us to the most important rule when implementing async/await: you must go async/await all the way in the call stack.

For instance, if the calling code blocks, does not implement async/await, and waits on the result with something like task.Result.ToString(), you'll end up with a deadlock. This is because the called async code will be trying to recapture the synchronization context upon return, but that synchronization context is being held by the calling (blocking) code.

To prevent the deadlock, the calling code has to be async and not block on the call. Go async/await all the way.

ConfigureAwait(false)

What if you don't care about returning to the calling context? In that case, as an optimization, async/await offers the .ConfigureAwait(false) to give you the flexibility of telling it that you don't care about returning to the same context and any thread from the thread pool will do. The only caveat with this is that you would have to add this method all the way in the call stack.

In summary, async/await is about writing non-blocking code. Implement async/await all the way to avoid deadlocks, and use .ConfigureAwait(false) when you don't care about returning to the calling context/thread.