Saveliy Bondini.NET Developer

5 Things you should know about async/await in C#


Asynchronous programming is becoming more and more popular, either in .Net or some other language. With asynchronous programming, your logic can be divided into available tasks, which can perform some long-running operations such as downloading a resource from the URL, reading a large file, performing a complex calculation or doing an API call without blocking the execution of your application on UI or service. To make you suffer less, .Net framework provides you simple and easy to use keywords, which are the async and await modifiers to transform your code from synchronous to asynchronous.

Even though async/await mechanism allows you to do complicated things easier, it is still quite a difficult subject by itself. The article sheds some light on some features and possible pitfalls that you may encounter writing async methods.

1. Return value
Async method signature can have three types of the return value: void, Task, and Task<T>. However, the way you return the value in asynchronous methods is not applicable here. Async methods implicitly create the instance of Task for you, which is used as a return value.

You might have noticed that we never return Task directly. Instead, we return nothing (in case of async void and Task method) or an instance of type T (if async Task<T> method). This simplification improves readability by making the asynchronous method look synchronous.
In the case of void return type, the instance of Task is still implicitly created, even though it is not returned to the caller.

Generally, it is a bad practice to have void async methods, unless they are used as asynchronous event handlers, where returning a value does not make much sense. Also if you want to implement the “fire and forget” strategy and do not care about it when it is finished. The last option is when you have to implement an interface that does not support Tasks. But in this case, you would rather ask the author of the interface to extend it.

2. Exception handling
Any thrown exception within a Task is always wrapped in AggregateException object. Therefore, working with Tasks, we usually deal with aggregate exceptions, which sometimes may be inconvenient.

Async/await simplifies exception handling to make code look more synchronous. When you await Task that was terminated by the exception, await mechanism implicitly unwraps propagated AggregateException, leaving you with the underlying exception.

Dealing with the unwrapped exceptions is easier and better in terms of code readability.

Sometimes AggregateException contains more than one exception. In this case, the first exception is taken from the inner exception list during unwrapping. If you want to handle all inner exceptions, you should do it directly through task object.

This is the reason why the code below works. If we change TimeoutException with ArgumentException in the catch statement, the thrown exception will be unhandled.

When you throw an exception inside async Task or async Task<T> method, it is attached to the task object. However, when you throw it inside the async void method, it cannot be attached to the task, as there is no task. Instead, the exception will be raised on the SynchronizationContext that was active when async void method started.

3. Async method inside
Async/Await allows us to write asynchronous code in a synchronous manner, therefore it is easy to labor under the delusion that invocation cost in terms of performance is also similar.

Unfortunately, it is far from the truth. Async/await is syntactic sugar, not available at IL/CLR level. For that reason compiler will generate state machine based on the method above, full of much boilerplate code.

The more awaits you have inside the method body, the bigger resulting state machine becomes, transforming what seems as several statements into a long chain of conditions and commands. It drastically increases invocation cost of such a method in comparison with synchronous method.

Another reason that may decrease performance is object allocations. Async/await causes implicit task object creation, required to be returned to the caller. This leads to one of the largest performance costs in an asynchronous method, which is calling a Garbage Collector. If such asynchronous method is frequently invoked it will result in a large number of created task objects needed to be garbage collected.

You can fix it by caching tasks and invoking the async method only when the cache does not have an appropriate task.

4. Context capturing
Async methods implicitly captures current synchronization context. Then, the context is used to execute the async method code after awaiting is finished. It means that the code before and after “await” statement will be executed on a thread owning captured context.

When does it prove useful? For example, in UI and ASP.NET applications, where you can modify or access objects only on the thread, that has created them. Without synchronization context, async method code will be executed using thread pool context on any currently available thread. (Fortunately, AspNetSynchronizationContext was removed in ASP.NET Core.

In addition, the async/await mechanism is highly optimized for the contextless scenario. There is simply less work to do for asynchronous requests. For more information you can read this comprehensive article by Stephen Cleary).

Here is an example of some WPF element event handler:

Here, we start running the method on a UI thread. After encountering await statement we return to the caller. The code following await statement would be executed after awaiting is finished. Thanks to the captured context it will be done on UI thread. If it were not for capturing context we would end up updating textbox object from a non-UI thread, that would lead us to an exception.

However, there are many cases when we do not need to switch contexts and execute code on a specific thread. Not only may it be unnecessary, but it also causes a performance loss. In this case, we can setup how we want to await an asynchronous operation using ConfigureAwait method.

The code above uses ConfigureAwait(false) to prevent context capturing, therefore after awaiting remaining code is executed on any available thread from the thread pool.

5. Awaiting different types
You might have noticed, in the previous example, we await an object that is not of Task type. It is because an object can be awaited if it implements the awaitable/awaiter pattern. This pattern expects from type:
1. Have a GetAwaiter() method, which returns the awaiter object
2. Returning awaiter must implement INotifyCompletion or ICriticalNotifyCompletion interface, have IsCompleted Boolean property and GetResult() method, which returns void, or a result.

For example, we can create asynchronous lazy initializer class:

As you can see, there are no dedicated interfaces or abstractions for awaitable/awaiter pattern in .NET. This allows us to implement GetAwaiter() method as an extension for existent types, making them awaitable.

Now we can await action delegate like we do this with tasks.

These are not all possible traps you can face working with Tasks. But keeping in mind all complexities around this fancy async/await mode, you will be ready to solve advanced issues such as deadlocks or race conditions.