Introduction
In Part 1 of the “Asynchronous programming with async and await in C#” series, we examined some of the theory behind async / await. In today’s Part 2 we are going to go even deeper and discuss about how async / await pattern looks exactly under the hood and we are going to demystify the concepts behind it.
To start today’s exploration, we are first going to distinguish between what is blocking and what is non-blocking code and we are going to see in which type does await fall into.
Blocking vs non-blocking code
Blocking code simply is code that blocks execution of the current thread for a specified amount of time or until another operation is completed. Other threads in the application may continue to execute, but the current thread does absolutely nothing until the other operation has completed. Another way to describe it is that the thread waits synchronously.
Some examples of blocking code constructs include:
• Thread.Sleep(numberOfMilliseconds)
System.Threading.Thread.Sleep(1000);
• task.GetAwaiter().GetResult() or task.Result property
var httpClient = new HttpClient();
var myTask = client.GetStringAsync("https://www.dotnetfoundation.org");
var myString = myTask.GetAwaiter().GetResult();
// or
// var myString = myTask.Result;
• task.Wait()
var httpClient = new HttpClient();
var myTask = client.GetStringAsync("https://www.dotnetfoundation.org");
myTask.Wait();
When dealing with non-blocking code, on the other hand, the current thread is free to do other things during the wait. So, based on what we saw on Part 1, await keyword is non-blocking.
Await from the developer’s perspective, means that the developer can provide other work for the original thread to do while awaiting some long-running operation. The fact that await frees the thread up to do other things, in case there is a GUI, means that the UI thread can remain responsive to additional user actions and input. Even if there is no GUI, for example in the case of ASP.NET, freeing up a thread potentially means greater scalability, allowing a single server to handle more requests concurrently.
Await from the perspective of the calling method, means that await doesn’t block and this non-blocking behavior returns control immediately to the calling method.
Before continuing further, let’s try a small exercise. I will give you a small code snippet and based on all the things we have already discussed about, I want you to pause for a bit and think about what exactly is the problem with this code:
Assume there is a method somewhere called ShowDialog that shows a message alert of some sort to the user
Ready? The problem here is that, the success dialog will display before the download/blur operation completes. Can you spot the reason behind this kind of behavior?
The reason is that, when a method using await (DownloadAndBlur) is not itself awaited (by OnButtonClick), execution of the calling method continues before the called method has completed. This simply means that the execution of DownloadAndBlur is performed synchronously, until the first encounter of await. Control at that time returns to the calling method as if DownloadAndBlur had already finished.
C# async / await demystified
Let’s try to demystify the async / await keywords. Assume you are calling a method called GetDataAsync() to fetch some data (e.g. from network) and get back a Task. Next you hook up a continuation on that and say when this operation is done, I want to call PutDataAsync() passing in the result of GetDataAsync. This will also return a Task. Finally after the second Task is complete, I want to print “done” in the console. Traditionally, to achieve something like this, we would need to write something like the following code:
Task<int> task1 = GetDataAsync();
task1.ContinueWith(a => {
var task2 = PutDataAsync(a.Result);
task2.ContinueWith(b => Console.WriteLine("done"));
});
How would this code be “translated” to async / await terms? We start again with calling GetDataAsync() method which returns a Task. Then we await this Task and this unwraps the result of this particular operation for us. We continue by calling PutDataAsync() passing in the result from the previous operation. Finally, we await the Task returned from PutDataAsync() and print “done” in the console:
Task<int> getDataTask = GetDataAsync();
int getDataTaskResult = await getDataTask;
Task putDataTask = PutDataAsync(getDataTaskResult);
await putDataTask;
Console.WriteLine("Done!");
The “traditional way” code we saw earlier is roughly what the compiler generates for us (in an over-simplified view), when we use the await keywords like in the previous code sample.
async & await keywords
An important thing to understand is that, all the “async” keyword does is it enables the use of the “await” keyword in that method and changes how method results are handled. It does not run this method on a thread pool thread.
The beginning of an async method is executed just like any other method. It runs synchronously until it hits an “await” keyword (or throws an exception). The “await” keyword is where things can get asynchronous.
Await is like a unary operator (e.g., i++): it takes a single argument, an awaitable (an “awaitable” is an asynchronous operation). There are two awaitable types already common in the .NET framework: Task<T> and Task.
Await examines that awaitable to see if it has already completed. If the awaitable has already completed, then the method just continues running synchronously, just like a regular method. If “await” sees that the awaitable has not completed, then it acts asynchronously.
It tells the awaitable to run the remainder of the method when it completes and then returns from the async method. When the awaitable completes, it will execute the remainder of the async method. If you’re awaiting a built-in awaitable (such as a Task), then the remainder of the async method will execute on a “captured context” that was captured before the “await” returned.
Think of “await” as an “asynchronous wait”. The async method pauses until the awaitable is complete (so it waits), but the actual thread is not blocked (so it’s asynchronous).
SynchronizationContext
When you await a built-in awaitable (like a Task or Task<T>), then the awaitable will capture the current “captured context” and later apply it to the remainder of the async method. The term “captured context” means that the CLR remembers the execution context before suspending the work at await and tries to re-apply it on the continuation.
If the main thread was a UI thread in a WPF application, the continuation runs on the same UI thread. For ASP.NET context, it might not be the same ASP.NET thread, but it’ll get back to the same HttpContext. For applications with no such context (e.g. console), the continuation runs on any ThreadPool thread.
Most of the time, you don’t need to sync back to the “main” context. You can tell the awaiter to not capture the current context by calling ConfigureAwait and passing false. Each “level” of async method calls has its own context.
Observe the example in the following image, which is taken from a WinForms application (works exactly the same for WPF), where we have the notion of a UI thread:
How synchronization context gets applied
This is what is happening here:
- DownloadFileButton_Click started in the UI context, and called DownloadFileAsync.
- DownloadFileAsync started in the UI context, but then stepped out of it with ConfigureAwait(false).
- The rest of DownloadFileAsync runs in the thread pool context.
- However, when DownloadFileAsync completes and DownloadFileButton_Click resumes, it does resume in the UI context.
Awaiter pattern
C# language compiles some of the features as syntactic sugar in a process called “Lowering”. Certain language features are just conveniences that translate into existing language features. The await expression, for example, is just one of those syntactic sugar features.
For a type to be awaitable:
• It needs to contain the following method: TaskAwaiter GetAwaiter().
/// <summary>Gets an awaiter used to await this <see cref="System.Threading.Tasks.Task"/>.</summary>
/// <returns>An awaiter instance.</returns>
public TaskAwaiter GetAwaiter()
{
return new TaskAwaiter(this);
}
• The return type of the GetAwaiter method needs to have the following: IsCompleted property of type bool and GetResult() method which returns void.
/// <summary>Gets whether the task being awaited is completed.</summary>
/// <remarks>This property is intended for compiler user rather than use directly in code.</remarks>
/// <exception cref="System.NullReferenceException">The awaiter was not properly initialized.</exception>
public bool IsCompleted => m_task.IsCompleted;
/// <summary>Ends the await on the completed <see cref="System.Threading.Tasks.Task"/>.</summary>
/// <exception cref="System.NullReferenceException">The awaiter was not properly initialized.</exception>
/// <exception cref="System.Threading.Tasks.TaskCanceledException">The task was canceled.</exception>
/// <exception cref="System.Exception">The task completed in a Faulted state.</exception>
[StackTraceHidden]
public void GetResult()
{
ValidateEnd(m_task);
}
Have a look at the Task class and you will see that it meets all above requirements and that is the reason that it indeed is an awaitable type (the above samples are taken from inside the Task class itself to be used as a reference).
Behind the compiler scenes
Now, let’s turn our attention to what actually happens under the hood and behind the compiler scenes when we use the async / await keywords in our C# code. As we have already seen, at some point, after an await has been encountered, a method needs to “wake up”, as it were, and continue with the rest of its code after the line that had the await keyword. If we were to look inside the call stack of an async method, we would see something like the following:
Snapshot of an async method call stack
Hmm, wait a second, what’s happening here? There are a lot of methods being called that we did not define in our code:
- AsyncStateMachineBox.MoveNext()
- AsyncTaskMethodBuilder.SetResult
This must mean that the compiler is generating a bunch of code on our behalf to keep track of the execution state. The truth is, whenever the compiler sees an async method in our code, it turns it into a state machine, which is actually a class.
This compiler generated class implements the IAsyncStateMachine interface. It encapsulates all the variables of your method as fields and splits your code into sections, that are executed as the state machine transitions between states, so that the thread can leave the method and when it comes back, the state is intact. This class is intended for compiler use only and has the following methods:
- MoveNext(): Moves the state machine to its next state.
- SetStateMachine(IAsyncStateMachine): Configures the state machine with a heap-allocated replica.
To observe what is actually happening, go ahead and visit sharplab.io website and in the “code” section on the left, paste the below code sample:
using System.Net.Http;
using System.Threading.Tasks;
namespace IOBoundOperation
{
class Demo
{
async Task<string> ReadDataFromUrl(string url)
{
var count = 10;
using var client = new HttpClient();
var page = await client.GetStringAsync(url);
if (count > page.Length)
{
return page;
}
return page.Substring(0, count);
}
}
}
By applying the “Lowering” process, let’s observe the so called state machine that is generated by the compiler, which is actually a class, as you can see in the below image:
The state machine (modeled as a class) that the compiler generates for us
This state machine is coded into a MoveNext method, is called for each step and keeps track of an integer state to execute the code. Let’s break down this MoveNext method a little bit more to examine what is actually happening behind the scenes for us by the compiler.
MoveNext method
In the above code sample we have declared a Task<string> ReadDataFromUrl(string url) async method and as we observed, the compiler will turn this method into a state machine wrapped inside a compiler autogenerated class.
Initially, the state machine inside the compiler-generated construct will execute the first 3 lines of the ReadDataFromUrl method, until the first encounter of the await keyword.
Developer code for ReadDataFromUrl
When the code enters the ReadDataFromUrl method, it will first create the HttpClient object and then it will issue the GetStringAsync request, using also the await keyword to “asynchronously wait” for the result to return before continuing with the rest of the method.
What will get executed initially inside the state machine
Then, using the if (!awaiter.IsCompleted) check, it will see whether the Task has already completed, and as we have mentioned above, in this case it will just continue its execution synchronously.
If the task has not yet completed, it will set the integer state of the state machine to 1 (num = (<>1_state = 0)), which instructs the state machine to move to the next state.
If the task has not yet completed, instruct the state machine to move to the next state
Also notice the return statement inside the if statement in the above image. Remember what we mentioned earlier: await from the perspective of the calling method, means that await doesn’t block and this non-blocking behavior returns control immediately to the calling method. This return statement here is the reason why this happens.
When the awaitable is complete and the result is ready, then the async method continues where it left off, processing the result that came from the GetStringAsync method call.
As you can see in the below image, the rest of the method (after the await keyword) will be outside of the if/else statement of the state machine.
Code after the await keyword — Process the result coming from the Task
One more thing to notice in this compiler autogenerated state machine is that, the whole logic of it is wrapped around a try-catch block. This means that all exceptions are caught and this is done even if your method does not have a try/catch handler. This is how await is able to re-throw the exceptions back to the client code.
Finally, after the try/catch block, we can see that the integer state of our the state machine gets resetted, to indicate that it is not running and the state machine is marked as having a result, which can be either a return value or an exception.
Resetting state value and mark Task as completed, either having a result or an exception
Conclusion
To sum up, what all these means is that the elegance of async/await comes at a price. Using async/await actually adds a bit of complexity (that you may not be aware). In server-side logic, this may not be critical, but particularly when programming mobile apps, where every CPU cycle and KB of memory counts, you should keep this in mind, as the amount of overhead can quickly add up.