https://www.markopapic.com/csharp-under-the-hood-async-await/express
Async and await keywords came with C# 5 as a cool new feature for handling asynchronous tasks.api
They allow us to specify tasks to be executed asynchronously in an easy and straightforward fashion.app
However, some people are mystified by asynchronous programming and are not sure how it actually works.async
I will try to give you an insight of the magic that happens under the hood when async and await are used.ide
C# language compiles some of the features as a syntactic sugar, which means that certain language features are just conveniences that translate into existing language features.ui
A lot of those syntactic sugar features expand into patterns.Those patterns are based on method calls, property lookups or interface implementations.this
await
expression is one of those syntactic sugar features. It leverages a pattern based on a few method calls. In order for a type to be awaitable, it has to meet the following requirements:spa
INotifyCompletion GetAwaiter()
GetAwaiter
method has to have the following:
IsCompleted
property of type bool
GetResult()
method which returns void
If you take a look at Task class, you will see that it meets all the above requirements.rest
1.被Await的class須要有一個GetAwaiter的方法code
2.而且GetAwaiter方法的返回類型,還有3個要求
2.1 必須實現INotifyCompletion接口
2.2 必須有一個IsCompleted屬性,類型是bool
2.3 必須有一個GetResult方法,返回值類型是void
So, a type doesn’t even need to implement some specific interface in order to be awaitable.
It just has to have a method with a specific signature. It is similar to duck typing.
If it walks like a duck and it quacks like a duck, then it must be a duck.
In this case it is
If it has certain methods with certain signatures, then it has to be awaitable.
To give you an illustrative example of this, I will create some custom class and make it awaitable.
So, here is my class:
public class MyAwaitableClass { }
When I try to await an object of MyAwaitableClass
type, I get the following error:
static async Task AwaitMyAwaitable() { MyAwaitableClass class1 = new MyAwaitableClass(); await class1; }
Severity Code Description Project File Line Suppression State
Error CS1061 'MyAwaitableClass' does not contain a definition for 'GetAwaiter' and no accessible extension method 'GetAwaiter' accepting a first argument of type 'MyAwaitableClass' could be found (are you missing a using directive or an assembly reference?) async-await-test C:\repository\GitHub\ChuckLu\Test\async-await-test\async-await-test\MainTest.cs 22 Active
Let’s add GetAwaiter
method to our class:
public class MyAwaitableClass { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } } public class MyAwaiter { public bool IsCompleted { get { return false; } } }
We can see that the compiler error changed:
Severity Code Description Project File Line Suppression State
Error CS0535 'MyAwaiter' does not implement interface member 'INotifyCompletion.OnCompleted(Action)' async-await-test C:\repository\GitHub\ChuckLu\Test\async-await-test\async-await-test\MyAwaitableClass.cs 6 Active
Ok, let’s create implement the INotifyCompletion
interface in MyAwaiter
:
public class MyAwaiter : INotifyCompletion { public bool IsCompleted { get { return false; } } public void OnCompleted(Action continuation) { } }
and see what the compiler error looks like now:
Severity Code Description Project File Line Suppression State
Error CS0117 'MyAwaiter' does not contain a definition for 'GetResult' async-await-test C:\repository\GitHub\ChuckLu\Test\async-await-test\async-await-test\MainTest.cs 18 Active
So, we add a GetResult
method and now we have the following:
public class MyAwaitableClass { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } } public class MyAwaiter : INotifyCompletion { public void GetResult() { } public bool IsCompleted { get { return false; } } //From INotifyCompletion public void OnCompleted(Action continuation) { } }
And we can also see that there are no compiler errors,
which means we have made an awaitable type.
Now that we know which pattern does the await
expression leverage, we can take a look under the hood to see what actually happens when we use async
and await
.
For every async method a state machine is generated.
This state machine is a struct that implements IAsyncStateMachine interface from System.Runtime.CompilerServices
namespace. This interface 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.Now let’s take a look at the following code:
class Program { static void Main(string[] args) { } static async Task FooAsync() { Console.WriteLine("Async method that doesn't have await"); } }
We have an async method named FooAsync
. You may notice that it lacks await operator, but I left it out for now for the sake of simplicity.
Now let’s take a look at the compiler generated code for this method. I am using dotPeek to decompile the containing .dll file. To see what is going on behind the scenes, you need to enable Show Compiler-generated Code option in dotPeek.
Compiler generated classes usually contain < and > in their names which are not valid C# identifiers so they don’t conflict with user-created artifacts.
也能夠用dnSpy查看反編譯的代碼
Let’s take a look what compiler generated for our FooAsync
method:
用ILSpy查看,在選項裏面,去掉反編譯async methods,確保看到狀態機的代碼
Our Program
class contains Main
and FooAsync
methods as expected, but we can also see that compiler generated a struct called Program.<FooAsync>d__1
.
That struct is a state machine that implements the IAsyncStateMachine
interface. Besides the IAsyncStateMachine
interface methods, this struct also has the following fields:
<>1__state
which indicates the current state of the state machine<>t__builder
of type AsyncTaskMethodBuilder
which is used for creation of asynchronous methods and returning the resulting task. The AsyncTaskMethodBuilder
struct is also intended for use by the compiler.We will see the code of this struct in more detail, but first let’s take a look at what compiler-generated FooAsync method looks like after we decompiled it:
[AsyncStateMachine(typeof(<FooAsync>d__1))] [DebuggerStepThrough] private static Task FooAsync() { <FooAsync>d__1 stateMachine = new <FooAsync>d__1(); stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); stateMachine.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder; <>t__builder.Start(ref stateMachine); return stateMachine.<>t__builder.Task; }
This is what compiler transforms async methods to. The code inside the method does the following:
AsyncTaskMethodBuilder
and set it as state machine’s builderAs you can notice, compiler-generated FooAsync
method doesn’t contain any of the code our original FooAsync
method had. That code represented the functionality of the method. So where is that code?
That code is moved to state machine’s MoveNext
method.
Let’s take a look at Program.<FooAsync>d_1
struct now:
[CompilerGenerated] private sealed class <FooAsync>d__1 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private void MoveNext() { int num = <>1__state; try { Console.WriteLine("Async method that doesn't have await"); } catch (Exception exception) { <>1__state = -2; <>t__builder.SetException(exception); return; } <>1__state = -2; <>t__builder.SetResult(); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } }
The MoveNext
method contains method’s code inside of a try
block.
If some exception occurs in our code, it will be given to the method builder which propagates it all the way to the task.
After that, it uses method builder’s SetResult method to indicate that the task is completed.
Now we saw how async methods look under the hood.
For the sake of simplicity, I didn’t put any await
inside of the FooAsync
method, so our state machine didn’t have a lot of state transitions.
It just executed our method and went to a completed state, i.e. our method executed synchronously.
Now it is time to see how MoveNext
method looks like when a method awaits some task inside of its body.
Let’s take a look at the following method:
static async Task BarAsync() { Console.WriteLine("This happens before await"); int i = await QuxAsync(); Console.WriteLine("This happens after await. The result of await is " + i); }
static async Task BarAsync() { Console.WriteLine("This happens before await"); HttpClient httpClient = new HttpClient(); Uri uri = new Uri("https://www.cnblogs.com/chucklu/"); var result = await httpClient.GetAsync(uri); var statusCode = result.StatusCode; Console.WriteLine($"This happens after await. The result of await is {statusCode}"); }
It awaits some QuxAsync method and uses its task result.
If we decompile it using dotPeek, we will notice that the compiler generated method has the same structure as FooAsync
even if the original methods are different:
[AsyncStateMachine(typeof(<BarAsync>d__2))] [DebuggerStepThrough] private static Task BarAsync() { <BarAsync>d__2 stateMachine = new <BarAsync>d__2(); stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); stateMachine.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder; <>t__builder.Start(ref stateMachine); return stateMachine.<>t__builder.Task; }
What makes the difference is the state machine’s MoveNext
method.
Now that we have an await
expression inside of our method, the state machine loks like this:
[CompilerGenerated] private sealed class <BarAsync>d__2 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private HttpClient <httpClient>5__1; private Uri <uri>5__2; private HttpResponseMessage <result>5__3; private HttpStatusCode <statusCode>5__4; private HttpResponseMessage <>s__5; private TaskAwaiter<HttpResponseMessage> <>u__1; private void MoveNext() { int num = <>1__state; //num variable holds the state try { TaskAwaiter<HttpResponseMessage> awaiter; if (num != 0)// The first state is -1(BarAsync method sets it to that value before starting the builder), so it will pass this check in the first pass { Console.WriteLine("This happens before await"); <httpClient>5__1 = new HttpClient(); <uri>5__2 = new Uri("https://www.cnblogs.com/chucklu/"); awaiter = <httpClient>5__1.GetAsync(<uri>5__2).GetAwaiter(); if (!awaiter.IsCompleted) //check if the asynchronous operation has already completed synchronously by the time we awaited {
//in case the method has not yet completed, do the following:
//1. set the state to 0(so next time it will not pass the check and go to else block below)
//2. register a callback using AwaitUnsafeOnCompleted, which eventually call the MoveNext method when the task completes
//3. return to prevent further execution num = (<>1__state = 0); <>u__1 = awaiter; <BarAsync>d__2 stateMachine = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } } else { awaiter = <>u__1; <>u__1 = default(TaskAwaiter<HttpResponseMessage>); num = (<>1__state = -1); } <>s__5 = awaiter.GetResult(); <result>5__3 = <>s__5; <>s__5 = null; <statusCode>5__4 = <result>5__3.StatusCode;
//The following line gets to be executed in the following case:
//1. The asynchronous operation has already completed by the time !task.Completed check happened
//2. The asynchronous operation has completed and MoveNext method is invoked as a callback. In this case state is 0, num!=0 check returns false
//It gets the task result and executes the rest of the method Console.WriteLine($"This happens after await. The result of await is {<statusCode>5__4}"); } catch (Exception exception) { <>1__state = -2; <>t__builder.SetException(exception); return; } <>1__state = -2; <>t__builder.SetResult(); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } }
The above red word contains an explanation of the above state machine:
So, what await
actually does is the following:
下面的num==0判斷是No的話,意味着第一次進入MoveNext
Every time you create an async method, the compiler generates a state machine for it. Then for each await
inside of that method, it does the following:
await
expression