C# async and await: A Deeper Dive
In my previous post, I introduced C#’s async and await keywords, described the need for asynchronous code, and explained advantages of the new asynchronous model over the tasks.
The simplicity of this model is based on the C# compiler that transforms async methods during project compilation. The compiler rewrites methods marked as async, introducing the boilerplate code required for the awaiting of IO operations and synchronization with the UI thread.
To understand how the code is transformed, let us create and compile two console applications:
The code on the left side represents applications created without using the async function, while the one on the right, the method is async. The next screenshot demonstrates the same applications disassembled using ILDASM:
In addition to all the methods present in first application, the second application contains a new member – a structure that orchestrates execution of asynchronous methods using a State Machine pattern. This structure encapsulates the functionality of the original DoSomething() method broken into stages and optimized for asynchronous execution. That leaves DoSomething() method’s only responsibility – to initialize and start the state machine.
These implementation details are not critical for day-to-day work, except the way the exceptions are handled by the re-written code: all exceptions generated in the async method are captured by the state machine and stored in the returning Task object. The stored exceptions will be raised if the async method was called using the await keyword, otherwise it will reside silently in the returned Task.
For example, the method defined above will generate an exception when called as await NewMethod() but will not affect the execution when called without await, as NewMethod().
Takeaway: always use await for calling async Task and async Task<T> methods, otherwise you may miss the exception.
Another way of defining the async method is to use async void declaration:
Here, there is no returned Task that can carry raised exceptions and there is no way for the caller to catch them. Exceptions generated by async void methods are directly propagated to the AppDomain (WPF, console applications) or Application (WinRT) level.
Takeaway: avoid async void as much as possible, use this declaration for event handlers only, and wrap handlers’ body in try/catch. Override global handler for the unhandled exceptions.
For an extremely low-level overview of async, see this presentation: Async Codegen.