I always wondered how the async and await really work. I could use them and understand mostly their flow but something was missing to fill the gap in my overall understanding. I did not understand how much power word await brings. Today I want to show you how async transforms your function in asynchronous code.
What do they do?
Async keyword added to your method basically let you use await keyword to wait for completion of some operation.
Await accepts any Task or Task<T> object and will not execute program flow until completion of the task. Usually the function will be accessing some I/O and in the result our calling thread (the one which used await) will be suspended until I/O operation completion. For example, you could access database in that time.
Calling method with await will not create new thread or something like that. If our current thread is waiting for operation completion then it is returning to thread pool. A place where your application has available threads for your application to use. Your thread may be used for another thing after being placed in the pool. Also your method may continue with different thread.
Of course new threads could be created inside invoked method or be a result of invoking the method.
It’s worth noting that this thread switching has its cost as your CPU needs to load registers with proper values and do specific works connected with loading execution context and synchronization context (you can read more about contexts [here]).
Async + await – what’s under the hood?
You finally have some glimpse of what those things do but async and await do a lot of magic. There can be also questions like:
- Why do we need async keyword?
- What really happens when we use await? What are side effects of that?
I have created simple program that uses async + await. It uses Task.Delay to simulate some asynchronous I/O operation. It’s worth pointing out that Task.Delay is far different from Thread.Sleep. Thread.Sleep will block the thread and it will be not free it to thread pool during waiting time.
Very simple code. I will only analyze what happens inside Main function. I will skip task function as it will be mainly the same :). And I will give first surprise. Async main function created yet another main function inside the class that is synchronous and is calling our main method and is waiting for its result(completion):
But. What happens with our main? It has completely changed into different thing because of async keyword. It is being sliced into parts and number of parts is being dictated by number of await keywords. Every part is then being transformed into a state and embedded into state machine associated with the function.
For example if we have 1 await keyword we will have 2 parts of our function and their role is to:
- Execute all code before async and await for completion of execution.
- Do everything after the await keyword.
For our example our state machine looks like this:
So let’s move to the MoveNext method as it is being most interesting part :).
At the start of MoveNext we have a switch that is used to figure out what to do for actual step.
The code is little messy with the gotos but it’s easy to understand. You can read it top-bottom if you will cover up if statements. It will be very similar to initial code then. Also, gotos were probably used here instead of inserting everything into switch as C# cannot go from one switch clause to another like in C++.
Every step has the same structure (Except last one) :
- 1. First it executes synchronous code in function.
- 2. Then it is calling the awaited method and getting TaskAwaiter object out of it. It’s needed to schedule execution of MoveNext again. This will be done by calling OnCompleted method in TaskAwaiter. It’s very similar to ContinueWith method inside Task.
- 3.A If task is completed then it’s going into a next step. It’s possible to execute all state machine’s steps in one run if everything will complete quickly.
- 3.B Otherwise we set a state and our task awaiter inside private variables. First one will remember which fragment of code to execute after going back again to MoveNext method. The awaiter could be also used to get result from the task in next step. (GetResult() is always used regardless whether method returns Task<T> or not).
- 4.B And finally by taskMethodBuilder we execute MoveNext on taskAwaiter again by calling AwaitUnsafeOnCompleted.
Last step is little different because it only has synchronous part. And at the end we return the result of async method and set the state to -2. The end. 🙂
It also has mechanism to catch exceptions. Whole StateMachine (if you did not see) is embraced in try/catch block.
From step 0 up (except last one) we are executing also GetResult on awaiter. Basically it does nothing if task is completed (or return result for TaskAwaiter<T>). Otherwise it waits for task completion. Personally I do not see reason for the task here not being completed.
Main – Last part
We still did not cover the last part. What has happened to the main? Now it looks like that:
Basically what it does is:
- It creates a state machine.
- It creates taskMethodBuilder for state machine and assign initial (-1) state for state machine.
- It starts the machine.
- It returns the task associated with state machine inside taskMethodBuilder.
This is all what is being done by async and await keywords. It’s very interesting how Microsoft made it. For me this knowledge was essential to understand async keyword.
If you would like to see full decompiled source code then it is available [here]. For clarity of article I changed names of variables to easier present a concept.
|I could not complete this post without great book called CLR via C#. Its chapter about asynchronous programming inspired me to write this blog post. I really recommend everyone using C# at intermediate/expert level to read this book as it gives enormous insight into everything. It just connect a lot of dots that could be unconnected in the past 🙂|