Understanding async/await in C#

Asynchronous programming is essentially having routines that returns immediately after being called. I was first exposed to this kind of calls with Ajax on the browser.

C# 5 introduced the async and await keywords which are very useful for making calls to asynchronous methods without breaking the linear flow of the program.

I had a hard time understanding async/await and especially the difference between the Task.Wait method and the await statement.

In this post, We will try to explain the very basic principle of async/await.

Methods returning Tasks

The TPL library allows us to create a Task object which represents a on going operation, the operation is usually executed in a different thread (but not always depending on the synchronization context).

Consider the following:

Creating a method that returns a task is basically creating an asynchronous method that will return its Task object immediately when it is called, as in the following example:

Whenever called the GetIntAsync method returns immediately, to get the result returned by the task we have to wait for it somehow.

Waiting synchronously

The Task class provides methods that allows us to wait synchronously for the operation completion, calling Task.Wait or referencing the Task.Result property will cause the current thread to block waiting for the completion of the task, consider the following:

Before proceeding to doing stuff with res the DoStuff method have to wait synchronously for the task completion.

This basically defeats the purpose of asynchrony which we use to get a chance to do something else on the same thread while an operation completes.

This is especially useful for IO bound operation that involves using high latency media such as networks and disks. Instead of waiting for IO in the current thread we can do useful work instead and wait for IO in another thread.

NodeJS is renowned to be very fast for IO bound workload because nearly all of its API is asynchronous!

Fortunately, we don’t have to wait synchronously each time we need the result of an asynchronous operation, we can schedule a callback that will be executed on task completion.

Scheduling a continuation (aka callback)

The Task class provides the ContinueWith method to which we can pass a statement lambda that will be executed on task completion, consider the following:

The DoStuff method does not block waiting for GetIntAsync to complete, instead it schedules a continuation to be executed when the task completes. Notice how the statement lambda gets res as an argument.

You can clearly see here that continuations are essentially callbacks and this is very similar to classic async programming that we can witness in various languages such as Javascript ES5 and Java for example.

This style of passing callback can get feisty in complex scenarii and we can end up with a callback hell in which we have to jump from callback to callback to follow the execution logic.

The await keyword allows to avoid this callback hell by basically “waiting without blocking” without breaking the linear flow of the program.

The Await keyword schedules continuations for us

Consider the example:

The await keyword is here used to call the asynchronous GetIntAsync method, notice here how we assigned the returned result directly to an int variable that can be used by the subsequent statements.

Note here that await is not blocking the current thread and the do something with res part is only executed when GetIntAsync is finished.

The await keyword basically instructs the C# compiler to:

  • take all statements after await
  • package them into a lambda
  • generate code that schedules the lambda for us

Instead of registering callbacks our selves, the await keywords allows us to delegate the work to the compiler.

This allows us to have a linear code flow while leveraging the benefits of asynchrony i.e. waiting without blocking.

Note also how we marked DoStuff as an async method returning a Task, all methods using the await keyword must be marked as async, since they are waiting without blocking they become asynchronous methods themselves and returns immediately when called.

We don’t have to deal with the generated code and of course in reality the process is more complex than described earlier, checkout the closing thoughts for pointers to details.

What happens when an exception is thrown?

The great thing about async/await is that most of the rest language feature work as expected, exception for example:

The thrown exception can be caught and the catch block will be executed when the exception is thrown in the asynchronous operation, so no worries.

Closing thoughts

async/await is one of the C# features that are boosting my productivity the most, fortunately more languages are adopting this construct like Javascript, Python and more.

If you are interested in more detail about async/await, I encourage you to check part 5 of C# in depth by Jon Skeet.

Also check out chapter 28 of CLR via C#, 4th Edition by Jeffrey Richter for a discussion about the benefits of asynchrony for IO bound operations.