The async/await keywords in C# are very much syntactical sugar that the compiler will use to generate the real code working behind async/await.
The async/await pattern is not a core part of the language, but is instead implemented with a state machine. Each async method will be translated into a state machine and then the calling method will use this state machine to execute business logic.
Example Code
Given the following method
public async Task PrintAndWait(TimeSpan delay, int arg2)
{
Console.WriteLine("Before first delay");
await Task.Delay(delay);
Console.WriteLine("Between delays");
await Task.Delay(delay);
Console.WriteLine("After second delay");
}
After compilation the method will look something like this
[AsyncStateMachine(typeof(<PrintAndWait>d__0))]
[DebuggerStepThrough]
public Task PrintAndWait(TimeSpan delay, int arg2)
{
<PrintAndWait>d__0 stateMachine = new <PrintAndWait>d__0();
stateMachine.<>4__this = this;
stateMachine.delay = delay;
stateMachine.arg2 = arg2;
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;
}
Tidying up the compiler generated code it will look like this
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
public Task PrintAndWait(TimeSpan delay, int arg2)
{
PrintAndWaitStateMachine stateMachine = new PrintAndWaitStateMachine()
{
Delay = delay,
Arg2 = arg2,
Builder = AsyncTaskMethodBuilder.Create(),
State = -1
};
stateMachine.Builder.Start(ref stateMachine);
return stateMachine.Builder.Task;
}
Notice the async
modifier is gone and the method body has been transformed to create and start a State Machine PrintAndWaitStateMachine
.
The compiler will also generate the PrintAndWaitStateMachine
class.
[CompilerGenerated]
private sealed class <PrintAndWait>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public TimeSpan delay;
public int arg2;
public C <>4__this;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
TaskAwaiter awaiter2;
if (num != 0)
{
if (num == 1)
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_00ef;
}
Console.WriteLine("Before first delay");
awaiter2 = Task.Delay(delay).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter2;
<PrintAndWait>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
}
else
{
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter2.GetResult();
Console.WriteLine("Between delays");
awaiter = Task.Delay(delay).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 1);
<>u__1 = awaiter;
<PrintAndWait>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
goto IL_00ef;
IL_00ef:
awaiter.GetResult();
Console.WriteLine("After second delay");
}
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);
}
}
Which when cleaned up will look like this
[CompilerGenerated]
class PrintAndWaitStateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
public TimeSpan delay;
public int arg2;
private TaskAwaiter _awaiter;
void IAsyncStateMachine.MoveNext()
{
int num = State;
try
{
TaskAwaiter awaiter;
TaskAwaiter awaiter2;
if (num != 0)
{
if (num == 1)
{
awaiter = _awaiter;
_awaiter = default(TaskAwaiter);
num = (State = -1);
goto IL_00ef;
}
Console.WriteLine("Before first delay");
awaiter2 = Task.Delay(delay).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (State = 0);
_awaiter = awaiter2;
PrintAndWaitStateMachine stateMachine = this;
Builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
}
else
{
awaiter2 = _awaiter;
_awaiter = default(TaskAwaiter);
num = (State = -1);
}
awaiter2.GetResult();
Console.WriteLine("Between delays");
awaiter = Task.Delay(delay).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (State = 1);
_awaiter = awaiter;
PrintAndWaitStateMachine stateMachine = this;
Builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
goto IL_00ef;
IL_00ef:
awaiter.GetResult();
Console.WriteLine("After second delay");
}
catch (Exception exception)
{
State = -2;
Builder.SetException(exception);
return;
}
State = -2;
Builder.SetResult();
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.Builder.SetStateMachine(stateMachine);
}
}
The delay
and arg2
parameters are now fields on the state machine class and the logic that was in the original PrintAndWait()
method is now inside the MoveNext()
method of the state machine. With the async modifier gone it is obvious that there is no IL/CLR level “async”, the compiler is merely transforming the code.
The State Machine
The generated state machine works by storing the current context (State) of the method so that it can be resumed after finishing it’s long running await tasks. Inside the PrintAndWaitStateMachine.MoveNext()
method we can see several checks for the current State (num
) value and calls to the method Builder.AwaitUnsafeOnCompleted()
MoveNext()
{
int num = State;
try
{
TaskAwaiter awaiter;
TaskAwaiter awaiter2;
if (num != 0)
{
if (num == 1)
{
awaiter = _awaiter;
_awaiter = default(TaskAwaiter);
num = (State = -1);
goto IL_00ef;
}
Console.WriteLine("Before first delay");
awaiter2 = Task.Delay(delay).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (State = 0);
_awaiter = awaiter2;
PrintAndWaitStateMachine stateMachine = this;
Builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
}
else
{
awaiter2 = _awaiter;
_awaiter = default(TaskAwaiter);
num = (State = -1);
}
awaiter2.GetResult();
Console.WriteLine("Between delays");
awaiter = Task.Delay(delay).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (State = 1);
_awaiter = awaiter;
PrintAndWaitStateMachine stateMachine = this;
Builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
goto IL_00ef;
IL_00ef:
awaiter.GetResult();
Console.WriteLine("After second delay");
}
catch (Exception exception)
{
State = -2;
Builder.SetException(exception);
return;
}
State = -2;
Builder.SetResult();
}
The generated code has been fragmented by each await
keyword that was used in the original method. So the method will be executed up to the first awaiter (await Task.Delay(delay)
) and if this awaiter has not been completed it will call AwaitUnsafeOnCompleted()
passing in the awaiter reference for the long running task and a reference to the current state machine. AwaitUnsafeOnCompleted()
will do several things including scheduling the state machine to proceed to the next action when the specified awaiter completes; this can be thought of as similar to callbacks or a wake up event.
After the AwaitUnsafeOnCompleted()
method is called then we return (or yield control over) to the calling method and the thread is released to do other things (possibly update a UI). When the awaiter has completed the “Wake Up Event” is triggered and the MoveNext()
method is executed again, this time it has an already present State so it will be able to move on to the next await task. From the code above it will follow the same flow to complete the second await Task.Delay(delay)
call.
States
- -2: The result of the method is computed, or it has thrown; we can really return now, and never come back
- -1: Start of “await Task.Delay(delay)”
- If it completed instantly, or if it done, keep going.
- If it hasn’t completed, wait till it ends, and return.
- 0 … N: These are generated based on the number of
await
keywords used in the original method.- In the code above only 2 awaits are used so states 1 & 2 are present in the StateMachine