Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Under the covers of the async modifier and await operator in .NET 4.5 and C# Metro style applications

Pete Brown - 21 May 2012

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.

image

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:

image

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)

         
posted by Pete Brown on Monday, May 21, 2012
filed under:          

Comment on this Post

Remember me