Saveliy Bondini.NET Developer

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


Even though async/await mechanism lets you do complicated things easier, it is still the complicated subject by itself. This article sheds some light on the 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 creates the Task instance for you, used as a method return value.

As you have noticed, we never return Task directly, instead we return nothing (in case of async void and Task method), or return the instance of T type (if async Task<T> method). This simplification improves readability by making an asynchronous method look as synchronous.
In case of void return type, the Task instance is still implicitly created, it is just 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.

2. Exception handling
Exception, thrown inside a Task is always wrapped in AggregateException object. Therefore, when 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 terminated by the exception, await mechanism implicitly unwraps propagated aggregate exception, leaving you with the underlying exception.
Dealing with the unwrapped exceptions is easier and better in terms of code readability.

Sometimes aggregate exception contains more than one exception. In this case, the first exception is taken from 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 I change TimeoutException with ArgumentException in catch statement, thrown exception will be unhandled.

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

3. Async method inside
Async/Await lets us write asynchronous code in a synchronous manner, therefore it is easy to labor under a 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. That is why compiler will generate state machine based on the method above, full of much boilerplate code.
The more awaits you have inside method body the bigger resulting state machine becomes,
transforming what seems as several statements into long chain of conditions and commands. It
increases invocation cost of such method drastically in comparison with synchronous method.
Another reason that may lower performance is object allocations. Async/await cause implicit task object creation, required to be returned to the caller. This leads to one of the largest performance cost in asynchronous method – resulting garbage collection. If such asynchronous method is frequently invoked it will result in a big amount of created task objects needed to be garbage collected.

You can fix it by caching tasks and invoking async method only when cache has no appropriate task.

4. Context capturing
Async methods implicitly captures current synchronization context. Context then used for executing async method code after awaiting is finished. It means that 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 thread, that created them. Without synchronization context, async method code will be executed using thread pool context on any currently available thread.
Here is an example of some WPF element event handler:

Here we start running 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 captured context it will be done on UI thread. If it were not for capturing context we would end up updating textbox object from non-UI thread, that would lead to an exception.
However, there are many cases when we do not need to switch contexts and execute code on some specific thread. Not only because it may be unnecessary, but it also may cause performance loss. In this case we can setup how we want to await asynchronous operation using ConfigureAwait method.

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

5. Awaiting different types
As you may have noticed, in previous example we await object that is not of Task type. This is because any object can be awaited if it implements awaitable/awaiter pattern. This pattern expects from type:
1. Have GetAwaiter() method, which returns an 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 have noticed there are no dedicated interfaces or abstractions for awaitable/awaiter pattern in .NET. This allows to implement GetAwaiter() method as extension for existent types, making them awaitable.

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

  • Stephen Wu

    It’s nice to see someone explaining some of the nuts and bolts of async/await like this but can all those “& lt;” and “& gt ;” codes be fixed because they are hard to read?