Nowadays, asynchronous programming isn’t something unusual. We all know that when it comes to do some I/O operations or HTTP request, we should do it async. Why? There’re several reasons but two most important are:
- Efficiency. In many cases, the code might work faster. That’s because it’s not forced to wait for the result. Instead, it can work on some other stuff and then come back for the response when it’s ready. Simple is that.
- Comfort. Async improves the comfort of using our applications. We don’t block the UI thread and we don’t have to wait for the completion of operations that might take a few minutes.
Of course, there are some scenarios when using async incorrectly or in wrong cases might cause a lot of trouble, but that’s the topic for the other article. In C#, the asynchronous programming can be achieved using the async/await keywords introduced in .NET 4.5. The example below presents its usage:
async Task<string> GetInvoiceAsync() { Task<dynamic> invoiceDataTask = GetDataAsync(); // Some Async method string invoiceNumber = CreateInvoiceNumber(); dynamic invoiceData = await invoiceDataTask; invoiceData.invoiceNumber = invoiceNumber; return invoiceData; } string CreateInvoiceNumber() { var random = new Random(); return $"{random.Next()}/{DateTime.Today.Year}"; }
The code looks almost identical to the synchronous version since all we did was just adding async/await keywords and wrapping the result in some weird object called Task. Let’s discuss what actually happened?
How does async method behave?
All right, the diagram below might help you understanding the flow of the entire code:
The steps are:
1. Some CALLER(which is another method) asynchronously calls GetInvoiceAsync method and awaits it.
2. The GetInvoiceAsync method calls GetDataAsync which gets needed data from file/database. What’s important here is the fact that the operation might take a while until it completes due to the connection or file access. Now this is the reason async programming is so cool. Alternatively (synchronously), we could stop processing here and wait for the result, but that’d not be efficient. In this case, instead of waiting, GetDataAsync method yields the control to the caller which in this case is GetInvoiceAsync. Now, that’s where Task<dynamic> comes into play. This object represents the ongoing operation (inside the GetDataAsync) which should return a dynamic type after its completion. It’s important to understand that the Task itself is not a result of the async method. Comparing to the JavaScript you can treat that as a Promise (that some operation will complete successfully and yield some result ). After creating a proper task, the method can process further.
3. Pending the result of the GetDataAsync method, we can do something in a meantime. That’s why the next step would be creating the invoice number synchronously (since it’s not marked as async). The result is then passed to its caller which is GetInvoiceAsync.
4. After creating the invoice number the GetInvoiceAsync method wants to assign the result of GetDataAsync method but the operation is not completed yet. Therefore it uses the awaitoperator to suspend the process and yield the control to the CALLER which might do some work while waiting for the invoice data (only if the GetInvoiceAsync was not awaited immediately).
5. GetDataAsync method completes and it produces the expected result. The catch here is that the result isn’t returned from the method itself. Why? Because it has already returned something – Task object. Therefore the result of the GetDatAsync method is stored inside Task object. After that, the await operator can retrieve it and assign to the invoiceData object.
6. The invoice number is assigned to the invoiceData object.
7. GetInvoiceAsync completes and it passes the result to the CALLER which can resume its processing.
The key issue here is to understand the difference between awaiting async method immediately and later in the code. In the example above we could await the GetDataAsync like this:
Task<dynamic> invoiceDataTask = await GetDataAsync();
The difference is that in the above case, we could not calculate the invoice number in the meantime since the await operator is treated like a point in the code where the result of the task is needed in order to move forward. I hope that’s clear for everybody who was not familiar with the async/await before Oh, one more thing! There’re some scenarios when we’re forced to await the async method immediately. The example here might be doing async calls to the database using Entity Framework (by „doing” I mean materializing IQueryable<T> using the ToListAsync, FirstOrDefaultAsync etc.). There wouldn’t be any compilation errors but the exception will be thrown in the runtime.
Knowing this crazy mechanism, sooner or later some questions might pop in your mind. Some of them are:
- What happens with async/await after compilation?
- How does the CLR know where to resume the processing after Task completion since it does not keep any state?
I guess, now is the time to find it out!
Decompiling async method
For the next parapraphs we’re going to use the following code:
class Program { static void Main(string[] args) { var result = TestAsync().Result; Console.ReadKey(); } static async Task<int> TestAsync() { Console.WriteLine("Init test method"); var firstResult = await GetNumberAsync(1); Console.WriteLine(firstResult); var secondResult = await GetNumberAsync(2); Console.WriteLine(secondResult + firstResult); var thirdResult = await GetNumberAsync(4); Console.WriteLine("I'm done"); return firstResult - secondResult + thirdResult; } public static async Task<int> GetNumberAsync(int number) => await Task.Run(() => number); }
There are several .NET decompilers including ildasm.exe (attached to the Visual Studio) but I’m going to use ILSpy. Having the program running I go to the File -> Open and I select the *.exe file produced by the compiler. After that an assembly should appear on the left side of the window:
Before we go any further, we need to do one more thing. To inspect what’s really inside the IL we need to uncheck one option in the setting of the ILSpy. You can find it under View -> Options -> Decompile Async Methods (async/await):
Now we are ready to inspect the code. The listing below presents most important parts of the decompiled C# program:
using Microsoft.CSharp.RuntimeBinder; using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace AsyncAwait { internal class Program { [CompilerGenerated] private sealed class <TestAsync>d__1 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder<int> <>t__builder; private int <firstResult>5__1; private int <secondResult>5__2; private int <thirdResult>5__3; private int <>s__4; private int <>s__5; private int <>s__6; private TaskAwaiter<int> <>u__1; void IAsyncStateMachine.MoveNext() { int num = this.<>1__state; int result2; try { TaskAwaiter<int> taskAwaiter; TaskAwaiter<int> taskAwaiter2; TaskAwaiter<int> taskAwaiter3; switch (num) { case 0: taskAwaiter = this.<>u__1; this.<>u__1 = default(TaskAwaiter<int>); this.<>1__state = -1; break; case 1: taskAwaiter2 = this.<>u__1; this.<>u__1 = default(TaskAwaiter<int>); this.<>1__state = -1; goto IL_117; case 2: taskAwaiter3 = this.<>u__1; this.<>u__1 = default(TaskAwaiter<int>); this.<>1__state = -1; goto IL_1A9; default: Console.WriteLine("Init test method"); taskAwaiter = Program.GetNumberAsync(1).GetAwaiter(); if (!taskAwaiter.IsCompleted) { this.<>1__state = 0; this.<>u__1 = taskAwaiter; Program.<TestAsync>d__1 <TestAsync>d__ = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<TestAsync>d__1>(ref taskAwaiter, ref <TestAsync>d__); return; } break; } int result = taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter<int>); this.<>s__4 = result; this.<firstResult>5__1 = this.<>s__4; Console.WriteLine(this.<firstResult>5__1); taskAwaiter2 = Program.GetNumberAsync(2).GetAwaiter(); if (!taskAwaiter2.IsCompleted) { this.<>1__state = 1; this.<>u__1 = taskAwaiter2; Program.<TestAsync>d__1 <TestAsync>d__ = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<TestAsync>d__1>(ref taskAwaiter2, ref <TestAsync>d__); return; } IL_117: result = taskAwaiter2.GetResult(); taskAwaiter2 = default(TaskAwaiter<int>); this.<>s__5 = result; this.<secondResult>5__2 = this.<>s__5; Console.WriteLine(this.<secondResult>5__2 + this.<firstResult>5__1); taskAwaiter3 = Program.GetNumberAsync(4).GetAwaiter(); if (!taskAwaiter3.IsCompleted) { this.<>1__state = 2; this.<>u__1 = taskAwaiter3; Program.<TestAsync>d__1 <TestAsync>d__ = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<TestAsync>d__1>(ref taskAwaiter3, ref <TestAsync>d__); return; } IL_1A9: result = taskAwaiter3.GetResult(); taskAwaiter3 = default(TaskAwaiter<int>); this.<>s__6 = result; this.<thirdResult>5__3 = this.<>s__6; Console.WriteLine("I'm done"); result2 = this.<firstResult>5__1 - this.<secondResult>5__2 + this.<thirdResult>5__3; } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(result2); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } } private static void Main(string[] args) { int result = Program.TestAsync().Result; Console.ReadKey(); } [DebuggerStepThrough, AsyncStateMachine(typeof(Program.<TestAsync>d__1))] private static Task<int> TestAsync() { Program.<TestAsync>d__1 <TestAsync>d__ = new Program.<TestAsync>d__1(); <TestAsync>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); <TestAsync>d__.<>1__state = -1; AsyncTaskMethodBuilder<int> <>t__builder = <TestAsync>d__.<>t__builder; <>t__builder.Start<Program.<TestAsync>d__1>(ref <TestAsync>d__); return <TestAsync>d__.<>t__builder.Task; } } }
I’m going to explain the code in the next paragraph but even though we can answer the first question from our list! Notice that async/await keywords disappeared which means that there are no such things like async and await instructions in the IL. The proposal is simple. It’s nothing more than a syntactic sugar. Now, let’s find out how does it really work.
It’s all about the state
Well, the first thing that catches the eye is the code size. It’s way bigger than the C# one. That is because the compiler generated (notice the CompilerGeneratedattribute) additional class in which the whole „magic” happens – state machine. This class implements an interface called IAsyncStateMachine, therefore it has its methods implemented (both explicitly). The methods are:
- SetStateMachine– in this case it does nothing
- MoveNext– it contains the whole logic taken from the GetInvoiceAsync method.
Besides methods, the state machine contains some private fields. If you take a closer look you might notice that their names are taken from the variables declared inside the GetInvoiceAsync method. What does it mean? All variables declared inside the async method has been moved inside the state machine as its fields.
Now, let’s take a look at the MoveNext method which consists mostly of one switch-case. Notice that the original C# code from the async method has been fragmented by await operator and each piece has been put into the separate case (which is nothing more than machine’s state).
What’s also worth mentioning is the goto statement inside each case block which optimizes the whole structure (since goto represents the unconditional jump in the memory). Below I presented the diagram of the state machine together with the description of the whole processing:
- A new instance of the state machine is created.
- The initial state is set to -1.
- MoveNext method is invoked.
- Method hits the switch-case. Since there’s no case for the -1 value it goes to the default one.
- GetNumberAsync is invoked with the parameter equal to 1. Machine checks whether the task is already completed. Let’s assume that it’s not.
- The machine’s state is set to 0.
- The internal state (TaskAwaiter<int>) is assigned to the machine’s private field.
- State machine schedules itself to proceed to the next action when the specified awaiter completes (using the AwaitUnsafeOnCompleted method).
- MoveNext method finishes and the thread is released.
That’s first „iteration” of the whole process. When the task completes it runs the proper thread using the CurrentSyncContext to finish the rest of the fragment (You can configure not to choose the specific thread using ConfigureAwaitmethod with the false parameter). Soon after, the MoveNext method is called once again (with another state) and the process starts again until it reaches the last lines of the method where the -2 state is set together with the calculated result. That happens for each async method in your code (each has its own state machine). And that’s the whole magic that stands beyond the async/await. At first, it may seem kind of complicated but believe me that’s it’s not that hard.
This concept isn’t used only for this particular purpose. Another C# example is yieldkeyword which is also nothing more than syntax sugar. On the IL level, it’s compiler-generated state-machine.
Share the post "What lies beneath async/await in C# ?"