In this article, you will see an analysis of what the C# compiler creates when it compiles a program, and then translate the compiled code back to C#.
Introduction
This essay will examine the internals of how the await
keyword works. We will do this by analyzing what the C# compiler creates when it compiles the following five line program:
public static async Task Main()
{
DoWork();
await Task.Delay(2000);
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
}
The program does some work by calling DoWork()
, then performs an await
, and then does some more work by calling MoreWork()
. The program ends by waiting for the user to press the Enter key.
We will analyze what the C# compiler creates when it compiles this program, and then translate the compiled code back to C#.
Table of Contents
- Full C# Program to Disassemble
- ildasm — C# Disassembler
- Class Program
- Constructor
- <Main> : void()
- Main : class [mscorlib]System.Threading.Tasks.Task() → SubMain()
- Display()
- DoWork()
- MoreWork()
- Class StateMachine
- Constructor
- SetStateMachine()
- MoveNext()
- Faithful C# Representation of Compiled Program
- Simplifying
- Simplifying Main()
- Simplifying MoveNext)()
- Simplifying SubMain()
- Simplified C# Representation
- Tracing What Happens During an Await
- Part 1
- Part 2
- Await in a Button_Click Event Handler
- Tracing AwawitUnsafeOnCompleted
- Timers
- Summary
- Related Article
- History
Full C# Program to Disassemble
Here is the full C# code of the program we will test:
namespace ConsoleAppTCSa
{
class Program
{
public static async Task Main()
{
DoWork();
await Task.Delay(2000);
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
}
private static void DoWork()
{
Display("Enter DoWork()");
Thread.Sleep(2000);
Display("Exit DoWork()");
}
private static void MoreWork()
{
Display("Enter MoreWork()");
Thread.Sleep(2000);
Display("Exit MoreWork()");
}
private static void Display(string s)
{
Console.WriteLine("{0:hh\\:mm\\:ss\\:fff} {1}", DateTime.Now, s);
}
}
}
We will compile this C# program to a *.exe file, then disassemble the *.exe file to see the CIL code the C# compiler created. (For programs compiled using .NET Core instead of .NET Framework, disassemble the *.dll file instead.)
CIL is "Common Intermediate Language", sometimes called MSIL – Microsoft Intermediate Language, or just IL – Intermediate Language. This is the assembly-like language the C# compiler outputs. If you're not familiar with this language, don't worry, I'll be explaining all the CIL code line by line. We're going to examine the CIL code and translate it back into a new C# program.
ildasm — C# Disassembler
I will be using Microsoft's ildasm.exe (Intermediate Language Disassembler) to examine the executable created by the C# compiler. (ildasm.exe is probably already on your computer somewhere. Search under C:\Program Files (x86)\Microsoft SDKs\Windows\... There are also other programs which can disassemble C# executables, such as ILSpy, dotPeek, and .NET Reflector.)
Here's what ildasm.exe shows when we load the compiled executable for the above program. I have added some C# annotations on the right:
The above is a list of classes and methods. The general format for methods is: Name : ReturnType(paramters)
. With great foresight, I will change some of the cryptic names given by ildasm.exe to something more user friendly and meaningful:
ildasm name: | | Rename to: |
<Main>d__0 | → | Class StateMachine |
<Main> | → | static void Main() |
Main | → | private static Task SubMain() |
<Main>d__0
is the name of a class the C# compiler created for us. I will rename that class to StateMachine
.
Also, we have two methods with similar names:
<Main>
(the angle brackets are part of the method name) Main
When we examine the details of these two methods, we will find that method <Main>
is the program's entry point, while method Main
is where the five lines of code in our above sample C# program method Main()
actually end up.
Class Program
We can now start to build a C# outline of our compiled program based on the IL DASM disassembler information above:
namespace ConsoleAppTCSa
{
class Program
{
class StateMachine
{
}
public Program()
{
}
static void Main()
{
}
private static void Display(string s)
{
}
private static void DoWork()
{
}
private static Task SubMain()
{
}
private static void MoreWork()
{
}
}
}
Let's go through the methods one by one.
.ctor : void()
Let's start by looking at the Constructor for class Program
.
Double-clicking on the .ctor : void()
line in the IL DASM window above opens a new window showing us the CIL code:
We can see this constructor is just the default constructor which doesn't do anything other than call the constructor for System.Object
, so we'll leave this out of our C# code and let the C# compiler generate this default constructor for us.
<Main> : void()
Let's now have a look at the code for method <Main> : void()
.
Double-clicking on <Main> : void()
in the IL DASM gives us:
At the top, we see this is a static
method which returns void
. The .entrypoint
declares this is where the program starts. In C#, this would be called method Main()
:
static void Main()
{
}
At the beginning, we see .locals
where it defines V_0
to be of type TaskAwaiter
:
static void Main()
{
TaskAwaiter V_0;
}
Breaking down the fist line of CIL code, we have:
command +-------------- return type --------------+ +---- called method ------+
↓ │ │ │ │
IL_0000: call class [mscorlib]System.Threading.Tasks.Task ConsoleAppTCSa.Program.Main
I will rename the method this line calls to SubMain()
, since we already have a Main()
method. This first line thus calls what I am renaming to SubMain()
and returns a Task
. We're going to end up with a couple of tasks so to help keep them straight, I will, with great foresight, name the task which gets returned here as statemachineTask
:
static void Main()
{
TaskAwaiter V_0;
Task statemachineTask = SubMain();
}
The second line takes the result of that call, which is statemachineTask
, and calls statemachineTask.GetAwaiter()
. We see the return type from this call is a TaskAwaiter
:
The instance
keyword indicates this is a class instance rather than a static
method. callvirt
is similar to call
with one difference being it also checks the validity of the class instance before using it. Since the TaskAwaiter
returned comes from statemachineTask
, I will name this statemachineTaskAwaiter
:
static void Main()
{
TaskAwaiter V_0;
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
}
(Note that internally statemachineTaskAwaiter
stores statemachineTask
in a private
field named m_task
. Thus, a TaskAwaiter
has access to the Task
it is performing the await
on.)
The third line:
IL_0005: stloc.0
stores the resulting statemachineTaskAwaiter
in local variable V_0
:
static void Main()
{
TaskAwaiter V_0;
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
V_0 = statemachineTaskAwaiter;
}
The 4th line, ldloca.s V_0
is "Load the address of local variable V_0
". Combining this with the 5th line:
not return
command static type +-------------------------- called method -----------------------+
↓ ↓ ↓ │ │
call instance void [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
Translates to calling V_0.GetResult();
static void Main()
{
TaskAwaiter V_0;
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
V_0 = statemachineTaskAwaiter;
V_0.GetResult();
}
The final line, ret
is of course a return
:
static void Main()
{
TaskAwaiter V_0;
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
V_0 = statemachineTaskAwaiter;
V_0.GetResult();
return;
}
Eliminating unnecessary local variable V_0
gives:
static void Main()
{
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
statemachineTaskAwaiter.GetResult();
return;
}
Main : class [mscorlib]System.Threading.Tasks.Task()
Let's move on to the other Main
CIL method:
Double-clicking on Main : class [mscorlib]System.Threading.Tasks.Task()
in the IL DASM gives us:
At the top, we see this is a private static
method which returns a Task
.
I will rename this CIL Main
method to SubMain()
, since we already have a Main()
method.
private static Task SubMain()
{
}
Line .locals
defines local variable V_0
to be of type <Main>d__0
, which is a class name. With great foresight, I have renamed this class to StateMachine
:
private static Task SubMain()
{
StateMachine V_0;
}
Translating the rest of this method from CIL to C# gives us:
private static Task SubMain()
{
StateMachine V_0;
V_0 = new StateMachine();
V_0.Builder = new AsyncTaskMethodBuilder();
V_0.State = -1;
V_0.Builder.Start(ref V_0);
return V_0.Builder.Task;
}
Removing the CIL code comments leaves us with:
private static Task SubMain()
{
StateMachine V_0;
V_0 = new StateMachine();
V_0.Builder = new AsyncTaskMethodBuilder();
V_0.State = -1;
V_0.Builder.Start(V_0);
return V_0.Builder.Task;
}
Renaming V_0
to statemachine
gives us:
private static Task SubMain()
{
StateMachine statemachine = new StateMachine();
statemachine.Builder = new AsyncTaskMethodBuilder();
statemachine.State = -1;
statemachine.Builder.Start(ref statemachine);
return statemachine.Builder.Task;
}
Display : void(string)
Double-clicking on Display : void(string)
in the IL DASM gives us:
This translates to:
private static void Display(string s)
{
Console.WriteLine("{0:hh\\:mm\\:ss\\:fff} {1}", DateTime.Now, s);
}
This outputs string s
preceded by a timestamp.
(Note: ldarg.0
here is not the 'this
' pointer because this is a static
method. If this were an instance method (something created with the new
keyword), then ldarg.0
would be the 'this
' pointer which all instance methods implicitly have as arg 0
. (You'll also see the word instance
in the CIL code near the top when it's an instance method instead of a static
method.))
DoWork : void()
Double-clicking on DoWork : void()
in the IL DASM gives us:
This translates to:
private static void DoWork()
{
Display("Enter DoWork()");
Thread.Sleep(2000);
Display("Exit DoWork()");
}
MoreWork : void()
Double-clicking on MoreWork : void()
in the IL DASM gives us:
This is similar to DoWork()
above and translates to:
private static void MoreWork()
{
Display("Enter MoreWork()");
Thread.Sleep(2000);
Display("Exit MoreWork()");
}
<Main>d__0 → class StateMachine
Let's open up class <Main>d__0
and see what's inside.
Again with great foresight, I will change some of the names here to ones which are more meaningful:
ildasm name: | | Rename to: |
<Main>d__0 | → | Class StateMachine |
<>1__state | → | State |
<>t__builder | → | Builder |
<>u__1 | → | DelayTaskAwaiter (because this gets created from delayTask returned from Task.Delay(2000) ) |
We see the general outline of this class is:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public StateMachine()
{
}
public void MoveNext()
{
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
.ctor : void()
Looking at the constructor for Class StateMachine
:
The code is:
We see this constructor is just the default constructor which doesn't do anything other than call the constructor for System.Object
, so we'll leave this out of our C# code and let the C# compiler generate a default constructor for us.
SetStateMachine
Skipping over the MoveNext()
method for a moment, let's have a look at the code for SetStateMachine
:
We can see this code does absolutely nothing. The only instruction is IL_0000: ret
, which is a return
statement. The only reason this method is here is because this class implements interface IAsyncStateMachine
which requires it. This translates to:
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
(Our C# version needs to be public
because it implements an interface requirement.)
MoveNext : void()
We now dive into the most complicated method of the whole program, the MoveNext()
method. This is a long one so bear with me, we'll get through it.
The CIL code for this method is:
Here is a line by line C# translation of the CIL code for MoveNext()
:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 V_0;
TaskAwaiter V_1;
StateMachine V_2;
Exception V_3;
V_0 = this.State;
try
{
if (V_0 == 0) goto IL_000c;
goto IL_000e;
IL_000c: goto IL_0052;
IL_000e:
DoWork();
Task delayTask = Task.Delay(2000);
V_1 = delayTask.GetAwaiter();
if (V_1.IsCompleted) goto IL_006e;
V_0 = 0;
this.State = 0;
this.DelayTaskAwaiter = V_1;
V_2 = this;
this.Builder.AwaitUnsafeOnCompleted(ref V_1, ref V_2);
goto IL_00bb;
IL_0052:
V_1 = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
V_0 = -1;
this.State = -1;
IL_006e:
V_1.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
goto IL_00a7;
}
catch (Exception exception)
{
V_3 = exception;
this.State = -1;
this.Builder.SetException(V_3);
goto IL_00bb:
}
IL_00a7:
this.State = -2;
this.Builder.SetResult();
IL_00bb:
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
Removing all the CIL code comments leaves us with:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 V_0;
TaskAwaiter V_1;
StateMachine V_2;
Exception V_3;
V_0 = this.State;
try
{
if (V_0 == 0) goto IL_000c;
goto IL_000e;
IL_000c: goto IL_0052;
IL_000e:
DoWork();
Task delayTask = Task.Delay(2000);
V_1 = delayTask.GetAwaiter();
if (V_1.IsCompleted) goto IL_006e;
V_0 = 0;
this.State = 0;
this.DelayTaskAwaiter = V_1;
V_2 = this;
this.Builder.AwaitUnsafeOnCompleted(ref V_1, ref V_2);
goto IL_00bb;
IL_0052:
V_1 = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
V_0 = -1;
this.State = -1;
IL_006e: V_1.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
goto IL_00a7;
}
catch (Exception ex)
{
V_3 = ex;
this.State = -1;
this.Builder.SetException(V_3);
goto IL_00bb;
}
IL_00a7:
this.State = -2;
this.Builder.SetResult();
IL_00bb:
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
We can simplify this a bit by examining these three lines of code at the top of the try
block:
if (V_0 == 0) goto IL_000c
goto IL_000e
IL_000c: goto IL_0052
If V_0
is zero, then we go to IL_000c
which in turn jumps to IL_0052
; otherwise we go to IL_000e
. This is a switch
statement, so let's simplify it now:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 V_0;
TaskAwaiter V_1;
StateMachine V_2;
Exception V_3;
V_0 = this.State;
try
{
switch (V_0)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
V_1 = delayTask.GetAwaiter();
if (V_1.IsComplete) goto IL_006e;
V_0 = 0;
this.State = 0;
this.DelayTaskAwaiter = V_1;
V_2 = this;
this.Builder.AwaitUnsafeOnCompleted(ref V_1, ref V_2);
return;
case 0:
V_1 = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
V_0 = -1;
this.State = -1;
IL_006e: V_1.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
V_3 = ex;
this.State = -1;
this.Builder.SetException(V_3);
return;
}
this.State = -2;
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
Rename local variables V_0
, V_1
, V_2
, V_3
with better names, and change label IL_006e:
to IsCompleted:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 state;
TaskAwaiter delayTaskAwaiter;
StateMachine statemachine;
Exception exception;
state = this.State;
try
{
switch (state)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
delayTaskAwaiter = delayTask.GetAwaiter();
if (delayTaskAwaiter.IsCompleted) goto IsCompleted;
state = 0;
this.State = 0;
this.DelayTaskAwaiter = delayTaskAwaiter;
statemachine = this;
this.Builder.AwaitUnsafeOnCompleted(ref delayTaskAwaiter, ref statemachine);
return;
case 0:
delayTaskAwaiter = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
state = -1;
this.State = -1;
IsCompleted: delayTaskAwaiter.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
exception = ex;
this.State = -1;
this.Builder.SetException(exception);
return;
}
this.State = -2;
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
We can simplify this a little bit more by localizing the definition of variable statemachine
and eliminating the unnecessary local variable named exception
:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 state;
TaskAwaiter DelayTaskAwaiter;
StateMachine statemachine;
Exception exception;
state = this.State;
try
{
switch (state)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
delayTaskAwaiter = delayTask.GetAwaiter();
if (delayTaskAwaiter.IsCompleted) goto IsCompleted;
state = 0;
this.State = 0;
this.DelayTaskAwaiter = delayTaskAwaiter;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted(ref awaiter, ref statemachine);
return;
case 0:
delayTaskAwaiter = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
state = -1;
this.State = -1;
IsCompleted: delayTaskAwaiter.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
exception = ex;
this.State = -1;
this.Builder.SetException(ex);
return;
}
this.State = -2;
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
Faithful C# Representation of Compiled Program
Putting this all together and arranging the methods in a logical order gives us our complete C# program:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleAppTCSa
{
class Program
{
static void Main()
{
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
statemachineTaskAwaiter.GetResult();
return;
}
private static Task SubMain()
{
StateMachine statemachine = new StateMachine();
statemachine.Builder = new AsyncTaskMethodBuilder();
statemachine.State = -1;
statemachine.Builder.Start(ref statemachine);
return statemachine.Builder.Task;
}
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 state;
TaskAwaiter delayTaskAwaiter;
state = this.State;
try
{
switch (state)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
delayTaskAwaiter = delayTask.GetAwaiter();
if (delayTaskAwaiter.IsCompleted) goto IsCompleted;
state = 0;
this.State = 0;
this.DelayTaskAwaiter = delayTaskAwaiter;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted
(ref delayTaskAwaiter, ref statemachine);
return;
case 0:
delayTaskAwaiter = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
state = -1;
this.State = -1;
IsCompleted: delayTaskAwaiter.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
this.State = -1;
this.Builder.SetException(ex);
return;
}
this.State = -2;
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
private static void Display(string s)
{
Console.WriteLine("{0:hh\\:mm\\:ss\\:fff} {1}", DateTime.Now, s);
}
private static void DoWork()
{
Display("Enter DoWork()");
Thread.Sleep(2000);
Display("Exit DoWork()");
}
private static void MoreWork()
{
Display("Enter MoreWork()");
Thread.Sleep(2000);
Display("Exit MoreWork()");
}
}
}
This is a faithful C# representation of what the compiler created when it compiled our original program. This code compiles and runs just fine, and it performs the same actions as the original code we started with. The main difference is this version does not use the async
/await
keywords.
Simplifying
Let's simplify our faithful C# representation of our compiled program in order to unveil the basic concepts of what's going on.
Simplifying Main()
Starting with method Main()
:
static void Main()
{
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
statemachineTaskAwaiter.GetResult();
return;
}
In this method, the second line:
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter()
returns a TaskAwaiter
. If we look at the code for Task.GetAwaiter()
in https://referencesource.microsoft.com, we will find that internally the returned TaskAwaiter
holds a pointer to task statemachineTask
in a private
field named m_task
.
The third line:
statemachineTaskAwaiter.GetResult()
internally calls statemachineTask.Wait()
and then checks the ending status of the task. We can simplify these two lines of code and eliminate the TaskAwaiter
:
static void Main()
{
Task statemachineTask = SubMain();
TaskAwaiter statemachineTaskAwaiter = statemachineTask.GetAwaiter();
statemachineTaskAwaiter.GetResult();
statemachineTask.Wait();
if (!statemachineTask.IsRanToCompletion) ThrowForNonSuccess(statemachineTask);
return;
}
The statemachineTask.Wait()
is accomplished by using Monitor.Wait(m_lock)
and Monitor.PulseAll(m_lock)
. The call to Monitor.Wait(m_lock)
does not return until another thread calls Monitor.PulseAll(m_lock)
.
First, we set the continuation action to run Monitor.PulseAll(m_lock)
, then we call Monitor.Wait(m_lock)
and wait.
static void Main()
{
private volatile object m_lock = new Object();
Task statemachineTask = SubMain();
statemachineTask.Wait();
statemachineTask.continuationObject = new Action(() =>
lock (m_lock)
{
Monitor.PulseAll(m_lock));
});
lock (m_lock)
{
Monitor.Wait(m_lock);
}
if (!statemachineTask.IsRanToCompletion) ThrowForNonSuccess(statemachineTask);
return;
}
When statemachineTask
transitions to the completed state, it runs the continuation action which calls Monitor.PulseAll(m_lock)
, which releases us from the wait.
Simplifying MoveNext()
Moving on to class StateMachine
method MoveNext()
, the call to if (delayTaskAwaiter.IsCompleted) goto IsCompleted;
is just an optimization to skip waiting for delayTask
to complete if it's already completed. Let's remove this optimization and the unnecessary optimizing local variables:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
private TaskAwaiter DelayTaskAwaiter;
public void MoveNext()
{
Int32 state;
TaskAwaiter delayTaskAwaiter;
state = this.State;
try
{
switch (this.State)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
TaskAwaiter delayTaskAwaiter = delayTask.GetAwaiter();
if (delayTaskAwaiter.IsCompleted) goto IsCompleted;
state = 0;
this.State = 0;
this.DelayTaskAwaiter = delayTaskAwaiter;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted
(ref delayTaskAwaiter, ref statemachine);
return;
case 0:
delayTaskAwaiter = this.DelayTaskAwaiter;
this.DelayTaskAwaiter = default;
state = -1;
this.State = -1;
IsCompleted: this.DelayTaskAwaiter.GetResult();
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
this.State = -1;
this.Builder.SetException(ex);
return;
}
this.State = -2;
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
This leaves us with:
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
public void MoveNext()
{
try
{
switch (this.State)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
TaskAwaiter delayTaskAwaiter = delayTask.GetAwaiter();
this.State = 0;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted
(ref delayTaskAwaiter, ref statemachine);
return;
case 0:
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
this.Builder.SetException(ex);
return;
}
this.Builder.SetResult();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
Examining the MoveNext() switch
statement case default:
which runs Part 1:
default:
DoWork();
Task delayTask = Task.Delay(2000);
TaskAwaiter delayTaskAwaiter = delayTask.GetAwaiter();
this.State = 0;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted(ref delayTaskAwaiter, ref statemachine);
return;
The line:
this.Builder.AwaitUnsafeOnCompleted(ref delayTaskAwaiter, ref statemachine);
sets the continuation to be run when task delayTask
completes. (Recall that delayTaskAwaiter
holds delayTask
in a private
field named m_task
.) The continuation to run when delayTask
completes is statemachine.MoveNext()
. We can conceptually replace this line with:
delayTask.continuationObject = new Action(() => this.MoveNext());
default:
DoWork();
Task delayTask = Task.Delay(2000);
TaskAwaiter delayTaskAwaiter = delayTask.GetAwaiter();
this.State = 0;
StateMachine statemachine = this;
this.Builder.AwaitUnsafeOnCompleted(ref delayTaskAwaiter, ref statemachine);
delayTask.continuationObject = new Action(() => this.MoveNext());
return;
Simplifying SubMain()
In SubMain()
, the line statemachine.Builder.Start(ref statemachine)
basically calls statemachine.MoveNext()
.
private static Task SubMain()
{
StateMachine statemachine = new StateMachine();
statemachine.Builder = new AsyncTaskMethodBuilder();
statemachine.State = -1;
statemachine.Builder.Start(ref statemachine);
statemachine.MoveNext()
return statemachine.Builder.Task;
}
The line return statemachine.Builder.Task;
returns a new task which Builder
creates. I'll call this task statemachineTask
. This task doesn't actually run anything, the task is only used as a flag to signal when the state machine has completed, and as a place to store a result value if there is one. This task is manipulated in two places by MoveNext()
:
this.Builder.SetResult();
This sets the statemachineTask
state to "RanToCompletion
" and calls the statemachineTask.continuationObject
action. this.Builder.SetException(ex);
This gets called if an exception is thrown while running Part 1 or Part 2 of the state machine. This adds the exception ex
to statemachineTask.m_exceptionsHolder
which is a list of exceptions, sets the statemachineTask
state to "Faulted
", and then calls the statemachineTask.continuationObject
action.
I'll pull statemachineTask
out of Builder
and place it in MoveNext()
. Then, we won't need Builder
anymore.
private static Task SubMain()
{
StateMachine statemachine = new StateMachine();
statemachine.Builder = new AsyncTaskMethodBuilder();
statemachine.State = -1;
statemachine.MoveNext();
return statemachine.Builder.Task;
return statemachine.statemachineTask;
}
class StateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
public Task statemachineTask = new Task();
public void MoveNext()
{
try
{
switch (this.State)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
this.State = 0;
delayTask.continuationObject =
new Action(() => this.MoveNext());
return;
case 0:
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
this.Builder.SetException(ex);
this.statemachineTask.m_stateFlags = m_stateFlags |
TASK_STATE_FAULTED;
Action action = this.statemachineTask.continuationObject as Action;
action();
return;
}
this.Builder.SetResult();
this.statemachineTask.m_stateFlags = m_stateFlags |
TASK_STATE_RAN_TO_COMPLETION;
Action action = this.statemachineTask.continuationObject as Action;
action();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
Simplified C# Representation
Putting this all together gives us a complete simplified version of our program which better shows what happens under the hood of an await
statement:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleAppTCSa
{
class Program
{
static void Main()
{
private volatile object m_lock = new Object();
Task statemachineTask = SubMain();
statemachineTask.continuationObject = new Action(() =>
lock (m_lock)
{
Monitor.PulseAll(m_lock));
});
lock (m_lock)
{
Monitor.Wait(m_lock);
}
if (!statemachineTask.IsRanToCompletion) ThrowForNonSuccess(statemachineTask);
return;
}
private static Task SubMain()
{
StateMachine statemachine = new StateMachine();
statemachine.State = -1;
statemachine.MoveNext();
return statemachine.statemachineTask;
}
class StateMachine : IAsyncStateMachine
{
public int State;
public Task statemachineTask = new Task();
public void MoveNext()
{
try
{
switch (this.State)
{
default:
DoWork();
Task delayTask = Task.Delay(2000);
this.State = 0;
delayTask.continuationObject =
new Action(() => this.MoveNext());
return;
case 0:
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
break;
}
}
catch (Exception ex)
{
this.statemachineTask.m_stateFlags = m_stateFlags |
TASK_STATE_FAULTED;
Action action = this.statemachineTask.continuationObject as Action;
action();
return;
}
this.statemachineTask.m_stateFlags = m_stateFlags |
TASK_STATE_RAN_TO_COMPLETION;
Action action = this.statemachineTask.continuationObject as Action;
action();
return;
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
return;
}
}
private static void Display(string s)
{
Console.WriteLine("{0:hh\\:mm\\:ss\\:fff} {1}", DateTime.Now, s);
}
private static void DoWork()
{
Display("Enter DoWork()");
Thread.Sleep(2000);
Display("Exit DoWork()");
}
private static void MoreWork()
{
Display("Enter MoreWork()");
Thread.Sleep(2000);
Display("Exit MoreWork()");
}
}
}
While this program will not compile because it directly references some private
fields as if they were public
, we can still trace what happens if the above program were to run. This will give us the insight we seek into how the await
keyword works.
Tracing What Happens During an Await
Following the code above, we can now trace the flow of our program:
Part 1
Main()
calls SubMain()
SubMain()
- creates
statemachine
- initializes
statemachine
to State = -1
- calls
statemachine.MoveNext()
MoveNext()
sees that this.State == -1
and jumps to case default:
where it runs Part 1:
DoWork()
is called Task.Delay(2000)
is called
Task.Delay(2000)
sets up a timer to expire in 2 seconds and returns delayTask
.
this.State
is set to 0
(so Part 2 will run next time MoveNext()
is called) delayTask.continuationObject
is set to this.MoveNext()
- returns to
SubMain()
SubMain()
returns statemachine.statemachineTask
to Main()
- At this point, we're all set up. All we have to do now is sit back and do nothing from here on.
“You don't always need to be getting stuff done. Sometimes, it's perfectly okay, and absolutely necessary, to shut down, kick back, and do nothing.” — Lori Deschene, Tiny Buddha website
The rest of our program will be handled by delayTask
when the timer expires. The continuation action for delayTask
is set to run MoveNext()
, which will run Part 2 of our code.
If this were a GUI program and Main()
was instead a Button_Click()
event handler, then we would ignore the returned statemachineTask
and just return to the Message Loop, allowing the Message Loop to handle other messages in the Message Queue. Since I chose a Console program for this demonstration, Main()
does not return at this point because the program would end. Instead, Main()
first waits for the returned statemachineTask
to complete.
Main()
waits for stateMachine
to complete:
- Sets
statemachineTask.continuationObject
to run Monitor.PulseAll(m_lock)
when statemachineTask
transitions to a completed state. - Calls
Monitor.Wait(m_lock)
This places the Main
thread in a waiting state. The Main
thread status is changed to "SleepJoinWait
", and the Main
thread is removed from the "running" queue, where threads receive CPU time, and placed in a "waiting" queue. The thread receives no further CPU time because it is no longer in the running queue.
Part 2
“Sitting quietly, doing nothing, spring comes, and grass grows by itself.” — Zen Proverb
- Sometime later, the 2 second timer expires and a
ThreadPool
thread runs the code which was set up to handle the timer expiration. This code calls the delayTask.continuationObject
action, which is MoveNext()
(this was set by MoveNext()
Part 1 after it called Task.Delay(2000)
and delayTask
was returned to it).
(If this were a GUI program, instead of directly calling MoveNext()
, the continuation action would be slightly more complex and first check to see if we needed to run the continuation action on the GUI thread, and if so, it would queue MoveNext()
to the Message Queue so MoveNext()
would be run on the GUI thread.)
MoveNext()
checks this.State
, sees that this.State == 0
, and jumps to case 0:
where it runs Part 2.
MoreWork()
is called. Console.Write("Press Enter: ")
is called. Console.ReadLine()
is called.
- The thread waits for the user to press the Enter key.
- The user presses the Enter key.
Console.ReadLine()
returns.
break;
We have now finished running both Part 1 and Part 2. All that is left to do now is cleanup.
statemachineTask
state is set to "RanToCompletion
". - The
statemachineTask.continuationAction
is called, which is Monitor.PulseAll()
(this was set earlier by Main()
when SubMain()
returned statemachineTask
to it).
Monitor.PulseAll(m_lock)
takes the thread that was placed in limbo via Monitor.Wait(m_lock)
, changes its state from "WaitSleepJoin
" back to "Running
", and returns the thread back to the "running" queue where it will start receiving CPU time slices again. Monitor.PulseAll(m_lock)
returns to MoveNext()
which called it.
MoveNext()
returns to its caller, which was the ThreadPool
, freeing up the thread.
- Meanwhile, the
Main
thread that was waiting via Monitor.Wait(m_lock)
has now been returned to the "running" queue and starts running again. The Main
thread checks the status of statemachineTask
to see if it ran successfully to completion, which it did. - The
Main
thread's next (and last) statement is return
, where it returns to the operating system which called Main()
in the first place. - The operating system performs a cleanup and terminates the program.
You may notice a potential race condition here, as at this point, we don't know if the ThreadPool
thread has completed its task and returned to the ThreadPool
yet. It turns out that releasing the Main
thread was the last thing the ThreadPool
thread needed to do. Once it releases the Main
thread, its job is done. It doesn't do anything useful after that. The Main
thread can go ahead and perform cleanup, and we don't really care about that ThreadPool
thread anymore or what state it's in.
And that's a detailed walkthrough of what happens when we run the seemingly simple 5 line program:
public static async Task Main()
{
DoWork();
await Task.Delay(2000);
MoreWork();
Console.Write("Press Enter: ");
Console.ReadLine();
}
Let's now have a look at what happens when await
is used in a WPF Button_Click()
event handler method. Assume we have a method that looks like:
private async void Button_Click(object sender, RoutedEventArgs e)
{
DoWork();
await Task.Delay(2000);
MoreWork();
}
This is similar to the 5 line console program we started with above. One difference is here, we have two parameters passed to us: sender
and e
. Both of these parameters happen to be unused in our sample code; however, they are available to us if we wanted to use them.
Compiling this code and then disassembling using the IL DISM disassembler, and then translating the CIL code back into C#, we will find that the compiler generated the CIL equivalent of:
private void Button_Click(object sender, RoutedEventArgs e)
{
Statemachine stateMachine = new Statemachine();
statemachine.Builder = new AsyncTaskMethodBuilder();
statemachine.This = this;
statemachine.Sender = sender;
statemachine.E = e;
statemachine.State = -1;
statemachine.Builder.Start(ref statemachine);
return;
}
This is nearly identical to method SubMain()
in our previous example above, except the code now also sets three more fields in statemachine
:
this
sender
e
The other difference is the method returns void
instead of returning statemachine.Builder.Task
as the console version did. The task statemachine.Builder.Task
is ignored in this scenario, and the thread returns to the Message Loop where it goes on to process other messages.
Everything else is the same as before. When we return from Button_Click()
, delayTask
has its continuationObject
set to run MoveNext()
, and statemachine.State == 0
so it's set to run the next part of the statemachine
(Part 2).
There is one other additional difference: the continuationObject
is now a special class which contains both the continuation Action
and also the SynchronizationContext
, which is what gets us back to the GUI thread if we were on the GUI thread to begin with and need to get back to it. If we don't need to jump back to the GUI thead, then MoveNext()
is called directly; if we do need to jump back to the GUI thread, then MoveNext()
is queued to the Message Queue. How this is accomplished, along with an explanation of the Message Queue and Message Loop, is explained in another essay of mine: Async/Await Explained with Diagrams and Examples: Getting Back to the UI Thread.
Tracing AwaitUnsafeOnCompleted
The following details are provided for anyone wishing to dig deeper into how the task continuation is set up for a WPF Button_Click()
event handler.
The line this.Builder.AwaitUnsafeOnCompleted(ref delayTaskAwaiter, ref statemachine);
creates an instance of class SynchronizationContextAwaitTaskContinuation
and stores that as the task.continuationObject
. The code in class Task
which executes this task.continuationObject
checks to see if the stored object is a TaskContinuation
, which SynchronizationContextAwaitTaskContinuation
derives from, and if so, it calls the taskContinuation.Run()
method. The Run()
method takes care of running the continuation action on the correct thread. The continuation action is still statemachine.MoveNext()
.
Tracing the call to AwaitUnsafeOnCompleted
, which passes delayTaskAwaiter
as awaiter
, and recalling that delayTaskAwaiter
holds delayTask
in local field m_task
, the internal calls are:
this.Builder.AwaitUnsafeOnCompleted(ref awaiter, ref statemachine);
awaiter.UnsafeOnCompleted(continuation);
TaskAwaiter.OnCompletedInternal(m_task, continuation,
continueOnCapturedContext:true, flowExecutionContext:false);
task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext,…
if (continueOnCapturedContext)
tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, continuationAction,…
tc
is our delayTask
task continuation, which gets set to a SynchronizationContextAwaitTaskContinuation
.
After calling AwaitUnsafeOnCompleted
, Button_Click()
returns to its caller, which is the Message Loop. The Message Loop then calls GetMessage()
which grabs the next message from the Message Queue. The next message may be a user input such as a keystroke or mouse click; or, if there are no messages in the Message Queue, then GetMessage()
checks to see if a timer has expired and if so it creates a WM_TIMER
message and returns that.
Timers
This brings up an interesting side note about timers which use the Message Queue. For every timer that is currently set, Windows maintains a counter value that it decrements on every hardware timer tick. When this counter reaches 0, Windows sets a timer expired flag in the appropriate application’s Message Queue header. When the Message Loop calls GetMessage()
, if the Message Queue is empty, GetMessage()
then checks the timer expired flag, and if it is set, GetMessage()
resets the timer expired flag, and generates and returns a WM_TIMER
message.
Since a check to see if a timer has expired happens only when the Message Queue is empty, it's possible for the WM_TIMER
message to be delayed if there are a lot of messages in the Message Queue, or if all the CPUs are currently busy doing other things, possibly things totally unrelated to the current running program.
It's even possible that multiple timer periods may expire before a WM_TIMER
message is returned from the Message Queue. However, only one WM_TIMER
message will be returned, and it will represent all expired periods.
It's also possible GetMessage()
could return a delayed WM_TIMER
message just before another time period expires, and, subsequently, return a second WM_TIMER
message almost immediately after the first.
Summary
This completes our deep dive under the hood look at what happens with the await
keyword. I hope this essay has been helpful.
History
- 11th March, 2022: Initial version