IL DASM (The Intermediate Language Disassembly tool) is
something I haven't used in a while. When .NET 1.0 first came out
in beta over a decade ago, a much younger me went and created a
"Hello World" in IL just to see how it's done. I still have it:
//
// Hello World IL Program
// ------------------------------------
// Written by Peter M. Brown
// August 15, 2001
//
// This is a minimal IL example. There are other options/attributes
// that can be used (and in many cases, should be used) but if it didn't
// prevent it from compiling, I bagged it :-)
//
// Requires Beta 2 version of ILASM and framework (1.0.2914)
//
// To assemble : ilasm HelloWorld.il
//
.assembly extern mscorlib
{}
.assembly HelloWorld
{}
.class public ILHelloWorld
{
// This is our main function the .entrypoint attribute is
// what defines the "Mainness" of Main. If you leave this
// attribute off, you will get an IL assembler error
//
// Entry point must be a static function
.method public static void Main()
{
.entrypoint
// print out a blank line
IL_0000: call void [mscorlib]System.Console::WriteLine()
// load our "Hello World" string
IL_0005: ldstr "Hello World! This is an IL program by Pete."
// print the string to the console using Console.Writeline
IL_0010: call void [mscorlib]System.Console::WriteLine(string)
// return from the function
IL_0015: ret // leave this out, and you'll get an exception
}
}
The executable for that still runs, in case there was any
question :). For those unfamiliar with IL, it's the common
intermediate language which is executed by the .NET runtime. When
you compile your C# or VB, or COBOL.NET applications, this is what
is produced. You can think of it almost like assembly for .NET,
although there isn't a 1:1 correspondence between actual machine
language and the opcodes here; the JIT compiler on the running
machine can optimize this IL in just about any way it see fit.
One way to get to IL DASM by opening up a developer command
prompt and typing "ildasm". Here's the tool showing the code I
wrote for this post, viewed in IL DASM from Visual Studio 11.
ILDasm gives you information on all the methods in your code.
It's a great way to see what the compiler produced for the runtime
to then optimize and JIT when you run the app. Being able to read
the generated IL using this tool can be helpful when you're trying
to figure out just what exactly the compiler is doing on your
behalf. In fact, that's exactly what I want to do here: figure out
how async and await are implemented under the covers.
NOTE
The examples in this post were created using a C# XAML
application on Visual Studio 11 beta on Windows 8 Consumer Preview.
However, you will see the same result in WPF 4.5. Later versions or
builds of .NET and Visual Studio may show very different IL.
Do not make any performance, scalability, or other key
assumptions based on this beta IL.
Normal synchronous method IL
Let's start by looking at the IL generated from a vanilla
synchronous method. The method sets an integer variable, calls two
methods, and writes a bunch of stuff to the output window.
private int DoSomething(string uri)
{
return uri.Length;
}
private int DoSomethingElse()
{
return DateTime.Now.Day;
}
private void BaselineNoAsync()
{
int i = 0;
var client = new HttpClient();
i = 1;
var getResult = DoSomething("");
Debug.WriteLine("Just completed first method");
i = 1;
var result = DoSomethingElse();
Debug.WriteLine("Just completed second method");
i = 1;
Debug.WriteLine(result);
Debug.WriteLine("i = " + i);
}
This code results in the following IL (just for the
BaselineNoAsync method):
.method private hidebysig instance void BaselineNoAsync() cil managed
{
// Code size 91 (0x5b)
.maxstack 2
.locals init ([0] int32 i,
[1] class [System.Net.Http]System.Net.Http.HttpClient client,
[2] int32 getResult,
[3] int32 result)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: newobj instance void [System.Net.Http]System.Net.Http.HttpClient::.ctor()
IL_0008: stloc.1
IL_0009: ldc.i4.1
IL_000a: stloc.0
IL_000b: ldarg.0
IL_000c: ldstr ""
IL_0011: call instance int32 AsyncUnderCover.BlankPage::DoSomething(string)
IL_0016: stloc.2
IL_0017: ldstr "Just completed first method"
IL_001c: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_0021: nop
IL_0022: ldc.i4.1
IL_0023: stloc.0
IL_0024: ldarg.0
IL_0025: call instance int32 AsyncUnderCover.BlankPage::DoSomethingElse()
IL_002a: stloc.3
IL_002b: ldstr "Just completed second method"
IL_0030: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_0035: nop
IL_0036: ldc.i4.1
IL_0037: stloc.0
IL_0038: ldloc.3
IL_0039: box [System.Runtime]System.Int32
IL_003e: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(object)
IL_0043: nop
IL_0044: ldstr "i = "
IL_0049: ldloc.0
IL_004a: box [System.Runtime]System.Int32
IL_004f: call string [System.Runtime]System.String::Concat(object,
object)
IL_0054: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_0059: nop
IL_005a: ret
} // end of method BlankPage::BaselineNoAsync
The code is relatively easy to understand. If you start at the
top, you can see the relationship between lines of C# code and the
lines of IL. For example, IL_0003 creates a new object and calls
its constructor. IL_0009, 0022, 0036 each push a 4 byte integer
(i4) with a value of 1, on to the stack. stloc reads from the stack
into that local variable. (I meant to do += instead of =,
but I didn't notice that until I had most of this post written.
It's not that important in any case as I was consistent in the two
examples here.)IL_000c loads the string "" and IL_0011
calls the DoSomething method with that string as a parameter. "nop"
is a no-op, and "ret" is the method return. You can also see our
old friend boxing in IL_004a when the integer (a value type) is
boxed as an object (a reference type).
Here are some of the opcodes shown in this example and the
next:
Opcode |
Description |
nop |
Space filler that does nothing. Could
possibly take a processing cycle. Sometimes used when you need to
rewrite code and not change addresses. Also used in debugging. |
stloc.<index> |
Pops the current value from the stack
and stores it in the local variable specified by the index |
stfld |
Replaces the value stored in the field
of an object reference or pointer with a new value (value is
current value on stack). |
newobj |
Creates a new object or a new instance
of a value type, pushing an object reference (type O) onto the
evaluation stack. |
ldc.i4.0, ldc.i4.1 |
Pushes a 4 byte integer value of 0 or
1 (respectively) on to the stack |
ldstr |
Pushes a new object reference to a
string literal stored in the metadata. |
ldloca.<index> |
Loads the address of the local
variable at a specific index onto the evaluation stack. |
ldarg.<index> |
Loads an argument (referenced by a
specified index value) onto the stack. |
box |
Converts a value type to an object
reference (type O). |
call |
Calls the method indicated by the
passed method descriptor. |
ret |
Returns from the current method,
optionally pushing a return value on to the stack |
leave |
Exits a protected region of code,
unconditionally transferring control to a specific target
instruction. |
br.s |
Unconditionally transfers control to a
target instruction (short form). |
brtrue.s |
Transfers control to a target
instruction (short form) if value on stack is true, not null, or
non-zero. |
Opcode descriptions mostly from this MSDN
page.
With our IL knowledge baselined, let's look at a similar method
which uses the async pattern and the async and await keywords for
asynchronous method calls.
Looking at async IL
Asynchronous methods are everywhere in .NET 4.5 and in the
Windows Runtime. To make it easier to make well-performing apps,
many WinRT and .NET 4.5 methods were made to work in an
asynchronous mode.
Let's use IL DASM to look at some asynchronous code. The
compiler performs some really interesting code rewriting when you
compile a method marked as async. Take, for example, the following
method:
private async void TestAsync()
{
int i = 0;
var client = new HttpClient();
i = 1;
var getResult = await client.GetAsync("");
Debug.WriteLine("Just completed Get");
i = 1;
var result = await getResult.Content.ReadAsStringAsync();
Debug.WriteLine("Just completed converting the result to a string");
i = 1;
Debug.WriteLine(result);
Debug.WriteLine("i = " + i);
}
That's pretty standard asynchronous code. However, note that I
return void. It's actually a better practice to make all async
methods return a Task unless it's really just an event handler. In
this case, it's a simple method that is called directly from an
event handler on the same page, so that's generally ok for a demo.
Nevertheless, consider return Task<T> as a best practice for
async code.
The code makes two asynchronous calls: The first to a URL, and
the second to get the results from the stream and read them into a
string. I added in the "i" local variable just to show some state
management for variables that are logically external to the
asynchronous operation code. That will be important when looking at
the IL itself.
In Visual Studio 11 beta, the compiler generates the following
IL for the above C# code using the async and await keywords:
.method private hidebysig instance void TestAsync() cil managed
{
.custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 )
// Code size 48 (0x30)
.maxstack 2
.locals init ([0] valuetype AsyncUnderCover.BlankPage/'<TestAsync>d__0' V_0,
[1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder V_1)
IL_0000: ldloca.s V_0
IL_0002: ldarg.0
IL_0003: stfld class AsyncUnderCover.BlankPage AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>4__this'
IL_0008: ldloca.s V_0
IL_000a: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
IL_000f: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_0014: ldloca.s V_0
IL_0016: ldc.i4.m1
IL_0017: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_001c: ldloca.s V_0
IL_001e: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_0023: stloc.1
IL_0024: ldloca.s V_1
IL_0026: ldloca.s V_0
IL_0028: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype AsyncUnderCover.BlankPage/'<TestAsync>d__0'>(!!0&)
IL_002d: br.s IL_002f
IL_002f: ret
} // end of method BlankPage::TestAsync
Note that given this is from the beta, and we're looking
at pretty deep implementation details, this is completely subject
to change in the future. That said, there are some
interesting things going on in this code, especially with the
generated d__0 state management class. Compare this to the code
earlier in this post which doesn't use async at all. At first
glance, this method looks much simpler, but seems to contain only
code which has no resemblance to the code we wrote.
Note also that these two comparisons (the synchronous and
asynchronous code) aren't 100% equivalent because I used methods in
the same class in one, and external methods in another. However,
they are close enough for this post.
What's happening
It may seem strange that the synchronous method is a lot longer
than the async method, and the async method has a bunch of foreign
code in it.
When you mark a method as async, the compiler rewrites
it so that state such as function-level variables are stored in a
structure (I incorrectly said this was a class, a
reference type, in one of my talks. It was in the async CTP, but
not in VS11). The state management structure tracks much more than
just that, however. It also manages the entry points for the
asynchronous calls, and contains the implementation code.
The key to understanding all of this is to look at the generated
state management class created when you compile a method as
async:
This structure has a number of fields and a couple methods, all
designed to let it work as a state machine which will be used to
track what's going on in your method. Specifically, it holds:
Field |
Description |
<>1__state |
This variable keeps track of where the
state machine is in its processing |
<>4__this |
Pointer to the class which contains
the async method. BlankPage in this case |
<>t__builder |
An async method builder used by the
async code in the state machine. |
<>t__stack |
This is for state that needs to be
persisted based on what was on the evaluation stack when the method
was suspended. For example, calling an async method passing in the
results of additional async calls as parameters. Stephen Toub said
this process when the stack field needs to get used is called
"spilling". |
<>u__$awaiter5 |
This is the TaskAwaiter which works
with the HttpResponse task. * |
<>u__$awaiter6 |
This is the TaskAwaiter which works
with the string task. * |
<client>5__2 |
The "client" local variable |
<getResult>5__3 |
The "getResult" local variable |
<i>5__1 |
The "i" local variable. |
<result>5__4 |
The final string result local
variable |
MoveNext |
This is the method with all the code.
It's the state machine |
SetStateMachine |
This is used to help with how the
struct gets boxed on to the heap. |
* Note that in this case, the two awaiters happen to be 1:1 with
the async tasks. If both tasks were of the same type, only one
awaiter would be declared (thanks to Stephen Toub for clearing this
up)
You may be wondering what happened with the Debug.WriteLine and
other statements we actually wrote. The MoveNext method has the
majority of the code that was pulled from our original method.
Here's the generated IL (warning: much code):
.method private hidebysig newslot virtual final
instance void MoveNext() cil managed
{
.override [System.Threading.Tasks]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext
// Code size 431 (0x1af)
.maxstack 3
.locals init ([0] bool '<>t__doFinallyBodies',
[1] class [System.Runtime]System.Exception '<>t__ex',
[2] int32 CS$4$0000,
[3] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> CS$0$0001,
[4] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> CS$0$0002,
[5] class [System.Net.Http]System.Net.Http.HttpResponseMessage CS$0$0003,
[6] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string> CS$0$0004,
[7] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string> CS$0$0005,
[8] string CS$0$0006)
.try
{
IL_0000: ldc.i4.1
IL_0001: stloc.0
IL_0002: ldarg.0
IL_0003: ldfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_0008: stloc.2
IL_0009: ldloc.2
IL_000a: switch (
IL_0019,
IL_001b)
IL_0017: br.s IL_0020
IL_0019: br.s IL_007f
IL_001b: br IL_010d
IL_0020: br.s IL_0022
IL_0022: nop
IL_0023: ldarg.0
IL_0024: ldc.i4.0
IL_0025: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<i>5__1'
IL_002a: ldarg.0
IL_002b: newobj instance void [System.Net.Http]System.Net.Http.HttpClient::.ctor()
IL_0030: stfld class [System.Net.Http]System.Net.Http.HttpClient AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<client>5__2'
IL_0035: ldarg.0
IL_0036: ldc.i4.1
IL_0037: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<i>5__1'
IL_003c: ldarg.0
IL_003d: ldfld class [System.Net.Http]System.Net.Http.HttpClient AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<client>5__2'
IL_0042: ldstr ""
IL_0047: callvirt instance class [System.Threading.Tasks]System.Threading.Tasks.Task`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> [System.Net.Http]System.Net.Http.HttpClient::GetAsync(string)
IL_004c: callvirt instance valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [System.Threading.Tasks]System.Threading.Tasks.Task`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>::GetAwaiter()
IL_0051: stloc.3
IL_0052: ldloca.s CS$0$0001
IL_0054: call instance bool valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>::get_IsCompleted()
IL_0059: brtrue.s IL_009d
IL_005b: ldarg.0
IL_005c: ldc.i4.0
IL_005d: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_0062: ldarg.0
IL_0063: ldloc.3
IL_0064: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter5'
IL_0069: ldarg.0
IL_006a: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_006f: ldloca.s CS$0$0001
IL_0071: ldarg.0
IL_0072: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted<valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>,valuetype AsyncUnderCover.BlankPage/'<TestAsync>d__0'>(!!0&,
!!1&)
IL_0077: nop
IL_0078: ldc.i4.0
IL_0079: stloc.0
IL_007a: leave IL_01ad
IL_007f: ldarg.0
IL_0080: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter5'
IL_0085: stloc.3
IL_0086: ldarg.0
IL_0087: ldloca.s CS$0$0002
IL_0089: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>
IL_008f: ldloc.s CS$0$0002
IL_0091: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter5'
IL_0096: ldarg.0
IL_0097: ldc.i4.m1
IL_0098: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_009d: ldloca.s CS$0$0001
IL_009f: call instance !0 valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>::GetResult()
IL_00a4: ldloca.s CS$0$0001
IL_00a6: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>
IL_00ac: stloc.s CS$0$0003
IL_00ae: ldarg.0
IL_00af: ldloc.s CS$0$0003
IL_00b1: stfld class [System.Net.Http]System.Net.Http.HttpResponseMessage AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<getResult>5__3'
IL_00b6: ldstr "Just completed Get"
IL_00bb: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_00c0: nop
IL_00c1: ldarg.0
IL_00c2: ldc.i4.1
IL_00c3: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<i>5__1'
IL_00c8: ldarg.0
IL_00c9: ldfld class [System.Net.Http]System.Net.Http.HttpResponseMessage AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<getResult>5__3'
IL_00ce: callvirt instance class [System.Net.Http]System.Net.Http.HttpContent [System.Net.Http]System.Net.Http.HttpResponseMessage::get_Content()
IL_00d3: callvirt instance class [System.Threading.Tasks]System.Threading.Tasks.Task`1<string> [System.Net.Http]System.Net.Http.HttpContent::ReadAsStringAsync()
IL_00d8: callvirt instance valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [System.Threading.Tasks]System.Threading.Tasks.Task`1<string>::GetAwaiter()
IL_00dd: stloc.s CS$0$0004
IL_00df: ldloca.s CS$0$0004
IL_00e1: call instance bool valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string>::get_IsCompleted()
IL_00e6: brtrue.s IL_012c
IL_00e8: ldarg.0
IL_00e9: ldc.i4.1
IL_00ea: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_00ef: ldarg.0
IL_00f0: ldloc.s CS$0$0004
IL_00f2: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter6'
IL_00f7: ldarg.0
IL_00f8: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_00fd: ldloca.s CS$0$0004
IL_00ff: ldarg.0
IL_0100: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted<valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string>,valuetype AsyncUnderCover.BlankPage/'<TestAsync>d__0'>(!!0&,
!!1&)
IL_0105: nop
IL_0106: ldc.i4.0
IL_0107: stloc.0
IL_0108: leave IL_01ad
IL_010d: ldarg.0
IL_010e: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter6'
IL_0113: stloc.s CS$0$0004
IL_0115: ldarg.0
IL_0116: ldloca.s CS$0$0005
IL_0118: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string>
IL_011e: ldloc.s CS$0$0005
IL_0120: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string> AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>u__$awaiter6'
IL_0125: ldarg.0
IL_0126: ldc.i4.m1
IL_0127: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_012c: ldloca.s CS$0$0004
IL_012e: call instance !0 valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string>::GetResult()
IL_0133: ldloca.s CS$0$0004
IL_0135: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1<string>
IL_013b: stloc.s CS$0$0006
IL_013d: ldarg.0
IL_013e: ldloc.s CS$0$0006
IL_0140: stfld string AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<result>5__4'
IL_0145: ldstr "Just completed converting the result to a string"
IL_014a: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_014f: nop
IL_0150: ldarg.0
IL_0151: ldc.i4.1
IL_0152: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<i>5__1'
IL_0157: ldarg.0
IL_0158: ldfld string AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<result>5__4'
IL_015d: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_0162: nop
IL_0163: ldstr "i = "
IL_0168: ldarg.0
IL_0169: ldfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<i>5__1'
IL_016e: box [System.Runtime]System.Int32
IL_0173: call string [System.Runtime]System.String::Concat(object,
object)
IL_0178: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::WriteLine(string)
IL_017d: nop
IL_017e: leave.s IL_0198
} // end .try
catch [System.Runtime]System.Exception
{
IL_0180: stloc.1
IL_0181: ldarg.0
IL_0182: ldc.i4.s -2
IL_0184: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_0189: ldarg.0
IL_018a: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_018f: ldloc.1
IL_0190: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetException(class [System.Runtime]System.Exception)
IL_0195: nop
IL_0196: leave.s IL_01ad
} // end handler
IL_0198: nop
IL_0199: ldarg.0
IL_019a: ldc.i4.s -2
IL_019c: stfld int32 AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>1__state'
IL_01a1: ldarg.0
IL_01a2: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder AsyncUnderCover.BlankPage/'<TestAsync>d__0'::'<>t__builder'
IL_01a7: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetResult()
IL_01ac: nop
IL_01ad: nop
IL_01ae: ret
} // end of method '<TestAsync>d__0'::MoveNext
Phew! That's a lot of code. If you take a high-level look at
what it's doing, however, it's implementing a state machine version
of your method. Essentially, this is what is happening:
Section |
Description |
IL_0003 - IL_0020 |
Check the value of the state variable
1__state. This is an integer. The value here decides where in the
code to jump to. |
IL_0024 - IL_0025 |
The code initializes the "i" variable
to 0. Note that this is a field in the state structure, not a local
variable. |
IL_002b - IL_0030 |
The code creates an instance of the
HttpClient class, and like the "i" variable, rather than storing it
as a local variable in the method, it stores it in a field in the
state structure. |
IL_0036 - IL_0037 |
Set the value of "i" to 1. |
IL_003d - IL_004c |
Call the first asynchronous
method. |
IL_0052 - IL_0059 |
Check to see if the call has completed
by calling get_IsCompleted(). If true, jump to the code which
checks if the next task awaiter has finished and has a result. |
IL_005d - IL_007a |
Set up all the awaiter business and
then exit the function, returning control to the caller. |
The rest of the code is similar to that. Basically, this
function is entered a number of times. Each time the location where
execution is picked up is decided by the switch statement at the
start, and whether get_IsCompleted() returns true. If the method
needs to suspend, the state structure will get boxed and stored on
the heap in order to preserve state. The whole process is pretty
ingenious.
More References
I only wanted to cover the IL weeds here. For more information
on the (decidedly more practical side of) async and await keywords,
and the async patterns in general, see these sites:
My Windows 8 book with Manning has an entire chapter on
working with asynchronous code from C#.
(Thanks also to Stephen Toub for correcting some assumptions I
had made in a draft of this post)