Introduction
This article is about the internal implementation of await
/async
functionality. It doesn’t cover the high level functionality, but aims to explore the internal implementation. If you would like to familiarise yourself with the overall functionality first there is an in depth article at Microsoft: https://msdn.microsoft.com/en-us/library/hh191443.aspx.
Sample diagram from Microsoft
Since this functionality was released, I was always curious about its internal implementation, this lead me to my forensic investigation and this article. I might have made several mistakes in my conclusions in the article, so any feedback in any form is welcome (even verbal abuse towards me :P).
Most of the conclusions are made by staring for hours at the IL code and trying to make sense of it all. I have included the IL code in case you want to double check my findings.
Overview of Async/Await
The Async functionality is implemented in the C# compiler together with some help from .NET framework base class libraries. The runtime itself didn’t have to be modified, although it’s entirely possible that some tweaks have been made to improve the performance of async
/await
. In simple terms this actually means that, the same functionality could be written by ourselves with the assistance of some helper classes in the earlier versions of .NET, however it doesn’t provide backwards compatibility.
The Stub Method
When you call an async
method the first thing that happens is that its stub is returned. For example for this method
public async Task<string> GetStringAsync(string value)
{
var result = value.ToUpper();
await Task.Delay(1000);
return result;
}
the compiler generates the following code (please see the IL file at the end of the article if you are very curious type):
public Task<string> GetStringAsync(string value)
{
AsyncClass.<GetStringAsync>d__0 <GetStringAsync>d__;
<GetStringAsync>d__.<>4__this = this;
<GetStringAsync>d__.value = value;
<GetStringAsync>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
<GetStringAsync>d__.<>1__state = -1;
AsyncTaskMethodBuilder<string> <>t__builder = <GetStringAsync>d__.<>t__builder;
<>t__builder.Start<AsyncClass.<GetStringAsync>d__0>(ref <GetStringAsync>d__);
return <GetStringAsync>d__.<>t__builder.Task;
}
Here the compiler initialises various variables of the state machine struct
and all the magic is implemented in the state machine. Struct is an obvious choice here, we don’t want to create an object on the heap each time the method is invoked, as this would significantly impair the performance of the async
methods by creating more work for GC.
The compiler uses unsupported naming conventions to ensure there are no conflicts. However, if we were to fix the variable names, in terms of the actual implementation there is nothing new and the code would actually compile in the previous versions of .NET Framework, if we were to implement the helper classes ourselves.
One other interesting observation is that the keyword async
has no effect on how the method is used from the outside. This is one of the reasons why you are cannot put async
keyword in interface method definitions and allows you to implement written using previous versions of .NET as long as they return Task, Task<T>
or void
.
Another thing to remember methods that return void
cannot be awaited. So even if your method is not returning anything you should return Task
unless you are writing a fire and forget method. The other interesting thing even though we call ToUpper()
on the string, this implementation is not included in the stub. The actual implementation has been moved to the state machine.
AsyncClass.il