This website uses cookies. By using the website you agree with our use of cookies. Know more


Asynchronous Programming

By André Canha
André Canha
Avid learner with lots of curiosity about lots of things. Love to run with my Nike Pegasus.
View All Posts
Asynchronous Programming
Asynchronous programming is ubiquitous nowadays, regardless of whether you’re developing for the web, mobile or desktop. The performance and responsiveness gains from using asynchronous code are almost always worth the benefit, even with minor drawbacks. One of those drawbacks is the complexity that asynchronous code brings. In the past few years, we've seen a rise in asynchronous programming. Thankfully, we've also seen best practices shared among peers, to help others avoid asynchronous programming pitfalls. An early best practice was to avoid eliding the async/await keywords from our methods, but this is not consensual advice that everyone supports. There is even a famous C# asynchronous specialist that first used to advise the eliding of the async/await keywords but later advised against it. This change of heart tells me that even someone with significant experience in this area sometimes gets things wrong, and it might be worth gaining a more in-depth understanding so I can have my own opinion or at least understand why someone would advise the use of eliding.

In this article, we’ll dive into the code transformation that the C# compiler does to generate asynchronous code in the hope of a better understanding of why to consider a best practice to avoid eliding the async/await keywords in most cases.

Before that, just a side note on an important concept called lowering in compilers. The term lowering is used when you take a high-level abstraction and rewrite it to a lower level. This allows us to have what we generally call syntactic sugar, and it gives us the ability to write more concise or idiomatic code. Let's look at an example in C#. If you have a list of elements, you can iterate through them using a foreach loop, something like this:

This is a piece of code that a developer might write on a daily basis, but under the hood of the C# language, the compiler will initially transform this code into the following:
It is not a complex transformation to understand but imagine that every time you want to write a foreach loop you would have to use the enumerator and express it with a while loop instead of having the more concise foreach. This is just a simple example but, the C# compiler does these kinds of transformations that allow us to be more productive and write less code. But sometimes, understanding what’s going on under the hood might be useful.

Before we start looking into the transformation that the compiler does with the async/await code, it's important to note that all the code transformations shown here were made under a release build. If you check the same code under a debug build, some things will be different. In a debug build the code is generated as a class to better support debug mode, while a release build will generate a struct for performance reasons.

Now that we understand this, let's look at what kind of transformation the C# compiler does with the async/await keywords. The first thing we should understand is that an async method is transformed into a state machine. The state machine can have the following states:

You’ll notice that if the method runs synchronously, then the flow is always the same; It goes from Not started to Executing to Completed. On the other hand, if the method runs asynchronously, then the flow depends on the number of awaits.

As an example, we are going to use a simple asynchronous method with two await expressions and a parameter.
We can use tools like Ildasm, RedGate Reflector or dotPeek to decompile .NET code, but I find that Sharplab is a good resource that you can use everywhere with a browser instead of needing to install an application. If you decompile the code below, you will find that those eight lines of code will be transformed into nearly 100 lines of code. Imagine that every time you wanted to code something as simple as the PrintAndWait method, you had to write 100 lines of code instead of just eight.

Let's now try to understand what kind of black magic is going on here. The compiler will generate some strange looking variables and class names but, I will simplify them here. The following code is logically identical to what the compiler generates. I've only omitted the code from the MoveNext() method.
The method in MyClass is the stub method which replaces the async method but without the async and await keywords. You may have noticed that none of our original code is in this method. All of our code will be placed inside the MoveNext method. The stub method is basically the state machine initialisation. 

The AsyncTaskMethodBuilder is a value type that is responsible for common async infrastructure, like setting the result or exceptions. The 'this' field is just the instance of MyClass, and the 'state' is always set to -1, which is the not started (or currently executing) state. The 'delay' field is the parameter from the original async method, and if we had more parameters, the compiler would have generated a field for each parameter in the state machine. After the state machine is created, the stub method calls the method Start(), and it passes the state machine by reference. This is an important optimization because it avoids a redundant copy of the state machine that, generally, is a fairly large struct. In the end, the stub method returns the task. So, as you can see, all the magic that allows a method to be asynchronous is implemented inside the MoveNext() method of the state machine struct.

Now that we have seen what the stub method does let's take a closer look at the state machine struct. We have already covered all of the public fields, so let's focus on the private field TaskAwaiter. A TaskAwaiter is created for each return task type we need inside our method. In our example, we only use a generic task, and so we only have a TaskAwaiter. Notice that we don't need a taskAwaiter for each await return type since the state machine can only await one return type at any time. This allows the compiler to reuse the same TaskAwaiter. Now let's suppose we had a method with four awaits that returns two generic Task one Task<int> and one Task<string>. The compiler would create three private fields, one TaskAwaiter, one TaskAwaiter<int> and one TaskAwiater<string>. It might sound confusing now but it will likely make more sense once we start to dig into what the MoveNext() method does. The SetStateMachine method is used to take care of boxing and storing the state machine in an efficient way.

Let's take a look at the code generated by the compiler for the MoveNext() method. To make it more readable, I’ll show you the same code logically and a little bit refactored. This refactoring is taken from the C# in Depth book by Jon Skeet, which I highly recommend.
As you can see, the MoveNext is where the original code is executed, and the execution can be synchronous or asynchronous. In a synchronous execution, all the awaits are completed, and the code can be executed without ever leaving the MoveNext. Let's suppose that all the awaits are complete and see what the flow would look like. The state initially is always -1, so in the switch, we go directly to the MethodStart label. The first Console.WriteLine is executed, and the first awaiter is complete, so we jump right to the GetFirstAwaitResult label. We get the result from the first await that is void, in this example, and the second Console.WriteLine is executed. Then, we call the second await, and the IsCompleted is true so we jump to the GetSecondAwaitResult label. Again, we get the result from the second await and execute the last Console.WriteLine. Finally, the state is set to -2, the completed state, and the result is set. In our case, there is no result to set as our function does not return a value.

Even if our method returned something instead of void, the MoveNext will always return void. An async method returned value will be handled by the builder and to return a value the compiler will use the SetResult of the builder. If our async method returned an int, our SetResult would look something like this:
Let’s now take a look at how this code would run asynchronously. The first steps are all the same until you reach the first await validation. In this flow, the await is not yet executed so the IsCompleted is false and instead of jumping for the result we have to set and store the state. Each state has its own number and when we need to wait for that execution to finish, we store that state to later resume at the same position on the code and continue with the execution. The state is set, to 0 in this case, as it is the await and then we use the AwaitUnsafeOnCompleted method from the builder. This method has the responsibility of signing up for the notification and other complicated work such as dealing with context. The important takeaway from this line is that when the task is completed, the execution will be resumed per the state machine and the MoveNext method will be called at the previous position that it was left at, with all the data that was stored. Once the task is completed the MoveNext is called again and this time the state is 0 so we jump right to the FirstAwaitContinuation label where we set the first await back to the variable, reset the instance await and set the state to the executing state (-1). From here, we proceed to the flow as in the synchronous execution. Now if the second await is already completed, we go through the synchronous flow and, if it is not completed, we go through the asynchronous flow again.

I hope that you now have a better understanding of all the work the compiler does for us when we use the async/await keywords. There’s still a lot that wasn’t covered in this article, such as error handling, branches, loops and custom awaitable types that make the state machine a bit more complicated, but with the basics covered, you should be able to better understand these more complex scenarios if needed. I hope this article helps you make more informed decisions as to whether you elide the async/await keywords in your asynchronous code.
Related Articles
How to build a recommender system: it's all about rocket science - Part 1

How to build a recommender system: it's all about rocket science - Part 1

By Diogo Gonçalves
Diogo Gonçalves
An engineer, a scientist, a sustainability lover and an AI geek craving for exploring the world with The North Face.
View All Posts
Paula Brochado
Paula Brochado
Astrophysicist of the galaxies, eternal pupil of arts, lover of (good) people, in a quest for all Adidas OG.
View All Posts