Technical note

.NET 11 killed the state machine

Fourteen years of async/await rested on an illusion. When you wrote an async method, it never existed as a method: the C# compiler chopped it into a state machine, a generated struct whose job was to simulate a coroutine that the runtime could not execute. With .NET 11, that era ends: the runtime itself now understands method suspension and resumption and, once the feature is enabled, the state machine disappears.

Microsoft presented it as one of the major advances of .NET 11: the feature, called runtime async, was announced as early as Preview 1 and detailed on the .NET Blog. The .NET runtime libraries are themselves now compiled with runtime-async=on and no longer contain any compiler-generated state machines. The .NET team is clearly betting on it.

I wanted to see the change with my own eyes, and you can too. Take the simplest possible program, compile it with the .NET 10 SDK, then with the .NET 11 SDK and the runtime-async feature enabled, and disassemble both DLLs with ildasm. The difference is spectacular.

The test program

Here is our guinea pig. Two async methods, a single await in each. Nothing simpler:

public class Program
{
    public static async Task Main()
    {
        await DoTaskAsynchronously();
    }

    private static async Task DoTaskAsynchronously()
    {
        await Task.Delay(100);
    }
}

Under .NET 10, the csproj is minimal: net10.0. Under .NET 11, we explicitly opt into the new feature:

<TargetFramework>net11.0</TargetFramework>
<Features>runtime-async=on</Features>

Then:

ildasm /tok /byt async-dotnet.dll /out=dump.il

.NET 10: the state machine

A state machine is an object that memorizes where a piece of work left off so it can continue later. That is exactly what the compiler generates for every async method since C# 5 (2012): the CLR of the time could not suspend and then resume a method, so Roslyn transforms your code into a generated struct, <Main>d__0, implementing the IAsyncStateMachine interface. Your method becomes nothing more than a 49-byte launcher stub: it instantiates the struct, creates an AsyncTaskMethodBuilder (the driver that produces the returned Task), and starts the machine.

The real body of your method lives in MoveNext(). Concretely, execution follows four steps:

  1. Launch. The Main stub instantiates <Main>d__0, initializes its state (<>1__state = -1), and calls builder.Start().
  2. First run. MoveNext unwinds your code up to the first await. If the task is already complete, execution continues without interruption.
  3. Suspension. Otherwise, the machine memorizes its resumption point (state = 0), stores the awaiter in a field, registers with the task via AwaitUnsafeOnCompleted, and returns. The method physically “exits” the call stack.
  4. Resumption. When the task completes, the runtime calls MoveNext again. The state field tells it to resume after the await: the awaiter confirms the task’s completion, and SetResult concludes the Task.

The following C# pseudocode approximately illustrates what the compiler generates to help the runtime. It is a simplification, but every element — the state field, the memorized awaiter, the try/catch routing exceptions to SetException — really exists in the IL:

// Pseudocode: approximation of what the compiler generates to help the runtime
struct <Main>d__0 : IAsyncStateMachine   // struct generated by the compiler
{
    public int <>1__state;               // where to resume (-1, 0, -2)
    public AsyncTaskMethodBuilder <>t__builder;
    private TaskAwaiter <>u__1;          // the awaiter memorized between suspensions

    void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)                 // first run
            {
                awaiter = DoTaskAsynchronously().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    <>1__state = 0;       // memorize the resumption point
                    <>u__1 = awaiter;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return;               // suspension: the method "exits" here
                }
            }
            else                          // resumption
            {
                awaiter = <>u__1;
                <>u__1 = default;
                <>1__state = -1;
            }
            awaiter.GetResult();
        }
        catch (Exception ex)
        {
            <>1__state = -2;
            <>t__builder.SetException(ex);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }
}

For our two one-line methods, the compiler produced two structs, two MoveNext implementations (156 and 154 bytes of IL), two SetStateMachine methods, six fields, two try/catch blocks, and two 49-byte launcher stubs. For Main alone, the generated IL totals 203 bytes: 49 for the stub, 154 for the MoveNext that holds the real body. Every variable that survives an await becomes a field. Every suspension becomes a state. That is the historical price of async.

This design aged remarkably well: it made async/await work everywhere, on runtimes that knew nothing about it. But it duplicates all this logic in every compiled async method, and the runtime remains, deep down, a spectator.

.NET 11: the CLR becomes async-aware

Now open the .NET 11 dump. Same program, same compiler, feature enabled. The contrast: the <Main>d__0 and <DoTaskAsynchronously>d__1 structs no longer exist. No IAsyncStateMachine, no builder, no <>1__state field. Even the <>c display class is gone: with no lambda, there is nothing left to generate at all. Only Main, DoTaskAsynchronously, and the constructors remain.

On the same basis as before, Main now follows three steps:

  1. Direct call. Main is a real method with 13 bytes of linear IL. It calls DoTaskAsynchronously() without instantiating any machine.
  2. Expressed suspension. Instead of a state machine, a single call: AsyncHelpers.Await(task). The method no longer “exits” itself: it expresses its suspension, and the CLR saves the stack state, the relevant locals, and will hand control back at the right place.
  3. Native resumption. The runtime restores the method and continues execution. No generated try/catch: exception propagation across suspension points is handled by the CLR. When the awaited task has a result, the generic overload Await<T> returns it directly.

In pseudocode, still as an approximation:

// Pseudocode: what Main becomes under .NET 11 — no generated struct
static Task Main()
{
    Task task = DoTaskAsynchronously();
    AsyncHelpers.Await(task);   // suspension expressed, execution delegated to the CLR
}

The comparison is striking on the same criteria: 203 bytes of generated IL for Main under .NET 10 (a 49-byte stub plus a 154-byte MoveNext) versus 13 bytes for a real method under .NET 11. Two generated structs versus zero. Two generated try/catch blocks versus zero. Even the process entry point changes: under .NET 10, the <Main>() stub blocked on GetAwaiter().GetResult(); under .NET 11, it delegates to AsyncHelpers.HandleAsyncEntryPoint. Everywhere, async has moved from the compiler back to the runtime.

What it changes in practice

For the same source code, the .NET 11 assembly contains fewer types, fewer methods, fewer fields, and fewer metadata entries. The JIT has less work. At runtime, the path is more direct: no state machine struct to handle, no builder.

The most interesting benefit concerns async call chains. When one runtime-async method awaits another, the runtime can optimize the handoff between the two and, in some cases, avoid allocating a Task entirely. Every intermediate layer of an async chain used to be a state machine composing with the next one; it is now a single CLR-driven execution.

Two honest caveats to finish. First, the observable semantics of your code do not change: this is an execution strategy evolution, not a language change. Second, the feature is opt-in in your csproj via <Features>runtime-async=on</Features> — but again, the .NET runtime libraries already use it by default. On the day it becomes the compiler’s default behavior, you will already know what happens under the hood.

Conclusion

The state machine was one of the most elegant tricks in C#’s history: convincing developers they had coroutines in a runtime that wanted none of it. .NET 11 makes it obsolete by moving asynchrony where it always belonged: into the runtime. The compiler now merely expresses suspension, AsyncHelpers.Await, and the CLR does the rest.

The complete IL dumps of both versions are in the appendix. Open them, compare, and above all run the experiment yourself: disassembling it yourself is how you truly measure the scale of the change.

Bibliography

Appendix

Full IL — .NET 10 (SDK 10, state machine)
// ildasm — async-dotnet.dll (.NET 10, SDK 10)
// Metadata version: v4.0.30319
// =============== CLASS MEMBERS DECLARATION ===================

.class  public auto ansi beforefieldinit Program
  extends [System.Runtime]System.Object
{
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8)  = ( 01 00 01 00 00 )
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8)  = ( 01 00 00 00 00 )
  .class  auto ansi sealed nested private beforefieldinit '<DoTaskAsynchronously>d__1'
  extends [System.Runtime]System.Object
  implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  {
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()  = ( 01 00 00 00 )
  .interfaceimpl type [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8)  = ( 01 00 00 00 00 )
  .field  public int32 '<>1__state'
  .field  public valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder '<>t__builder'
  .field  private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'
  .method  public hidebysig specialname rtspecialname
  instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
  // Method begins at RVA 0x20f7
  // Code size  8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call  instance void [System.Runtime]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  ret
  } // end of method '<DoTaskAsynchronously>d__1'::.ctor

  .method  private hidebysig newslot virtual final
  instance void  MoveNext() cil managed
  // SIG: 20 00 01
  {
  .override [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext
  // Method begins at RVA 0x2100
  // Code size  156 (0x9c)
  .maxstack  3
  .locals  init (int32 V_0,
  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_1,
  class Program/'<DoTaskAsynchronously>d__1' V_2,
  class [System.Runtime]System.Exception V_3)
  IL_0000:  ldarg.0
  IL_0001:  ldfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_0006:  stloc.0
  .try
  {
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_000c

  IL_000a:  br.s  IL_000e

  IL_000c:  br.s  IL_0049

  IL_000e:  nop
  IL_000f:  ldc.i4.s  100
  IL_0011:  call  class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Delay(int32)
  IL_0016:  callvirt  instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
  IL_001b:  stloc.1
  IL_001c:  ldloca.s  V_1
  IL_001e:  call  instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
  IL_0023:  brtrue.s  IL_0065

  IL_0025:  ldarg.0
  IL_0026:  ldc.i4.0
  IL_0027:  dup
  IL_0028:  stloc.0
  IL_0029:  stfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_002e:  ldarg.0
  IL_002f:  ldloc.1
  IL_0030:  stfld  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<DoTaskAsynchronously>d__1'::'<>u__1'
  IL_0035:  ldarg.0
  IL_0036:  stloc.2
  IL_0037:  ldarg.0
  IL_0038:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_003d:  ldloca.s  V_1
  IL_003f:  ldloca.s  V_2
  IL_0041:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::AwaitUnsafeOnCompleted<valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter,class Program/'<DoTaskAsynchronously>d__1'>(!!0&,
  !!1&)
  IL_0046:  nop
  IL_0047:  leave.s  IL_009b

  IL_0049:  ldarg.0
  IL_004a:  ldfld  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<DoTaskAsynchronously>d__1'::'<>u__1'
  IL_004f:  stloc.1
  IL_0050:  ldarg.0
  IL_0051:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<DoTaskAsynchronously>d__1'::'<>u__1'
  IL_0056:  initobj  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter
  IL_005c:  ldarg.0
  IL_005d:  ldc.i4.m1
  IL_005e:  dup
  IL_005f:  stloc.0
  IL_0060:  stfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_0065:  ldloca.s  V_1
  IL_0067:  call  instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
  IL_006c:  nop
  IL_006d:  leave.s  IL_0087

  }  // end .try
  catch [System.Runtime]System.Exception
  {
  IL_006f:  stloc.3
  IL_0070:  ldarg.0
  IL_0071:  ldc.i4.s  -2
  IL_0073:  stfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_0078:  ldarg.0
  IL_0079:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_007e:  ldloc.3
  IL_007f:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetException(class [System.Runtime]System.Exception)
  IL_0084:  nop
  IL_0085:  leave.s  IL_009b

  }  // end handler
  IL_0087:  ldarg.0
  IL_0088:  ldc.i4.s  -2
  IL_008a:  stfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_008f:  ldarg.0
  IL_0090:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_0095:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetResult()
  IL_009a:  nop
  IL_009b:  ret
  } // end of method '<DoTaskAsynchronously>d__1'::MoveNext

  .method  private hidebysig newslot virtual final
  instance void  SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine) cil managed
  // SIG: 20 01 01 12 59
  {
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerHiddenAttribute::.ctor()  = ( 01 00 00 00 )
  .override [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine
  // Method begins at RVA 0x21b8
  // Code size  1 (0x1)
  .maxstack  8
  IL_0000:  ret
  } // end of method '<DoTaskAsynchronously>d__1'::SetStateMachine

  } // end of class '<DoTaskAsynchronously>d__1'

  .class  auto ansi sealed nested private beforefieldinit '<Main>d__0'
  extends [System.Runtime]System.Object
  implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  {
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()  = ( 01 00 00 00 )
  .interfaceimpl type [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8)  = ( 01 00 00 00 00 )
  .field  public int32 '<>1__state'
  .field  public valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder '<>t__builder'
  .field  private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'
  .method  public hidebysig specialname rtspecialname
  instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
  // Method begins at RVA 0x21ba
  // Code size  8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call  instance void [System.Runtime]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  ret
  } // end of method '<Main>d__0'::.ctor

  .method  private hidebysig newslot virtual final
  instance void  MoveNext() cil managed
  // SIG: 20 00 01
  {
  .override [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext
  // Method begins at RVA 0x21c4
  // Code size  154 (0x9a)
  .maxstack  3
  .locals  init (int32 V_0,
  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_1,
  class Program/'<Main>d__0' V_2,
  class [System.Runtime]System.Exception V_3)
  IL_0000:  ldarg.0
  IL_0001:  ldfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_0006:  stloc.0
  .try
  {
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_000c

  IL_000a:  br.s  IL_000e

  IL_000c:  br.s  IL_0047

  IL_000e:  nop
  IL_000f:  call  class [System.Runtime]System.Threading.Tasks.Task Program::DoTaskAsynchronously()
  IL_0014:  callvirt  instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
  IL_0019:  stloc.1
  IL_001a:  ldloca.s  V_1
  IL_001c:  call  instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
  IL_0021:  brtrue.s  IL_0063

  IL_0023:  ldarg.0
  IL_0024:  ldc.i4.0
  IL_0025:  dup
  IL_0026:  stloc.0
  IL_0027:  stfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_002c:  ldarg.0
  IL_002d:  ldloc.1
  IL_002e:  stfld  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<Main>d__0'::'<>u__1'
  IL_0033:  ldarg.0
  IL_0034:  stloc.2
  IL_0035:  ldarg.0
  IL_0036:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_003b:  ldloca.s  V_1
  IL_003d:  ldloca.s  V_2
  IL_003f:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::AwaitUnsafeOnCompleted<valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter,class Program/'<Main>d__0'>(!!0&,
  !!1&)
  IL_0044:  nop
  IL_0045:  leave.s  IL_0099

  IL_0047:  ldarg.0
  IL_0048:  ldfld  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<Main>d__0'::'<>u__1'
  IL_004d:  stloc.1
  IL_004e:  ldarg.0
  IL_004f:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter Program/'<Main>d__0'::'<>u__1'
  IL_0054:  initobj  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter
  IL_005a:  ldarg.0
  IL_005b:  ldc.i4.m1
  IL_005c:  dup
  IL_005d:  stloc.0
  IL_005e:  stfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_0063:  ldloca.s  V_1
  IL_0065:  call  instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
  IL_006a:  nop
  IL_006b:  leave.s  IL_0085

  }  // end .try
  catch [System.Runtime]System.Exception
  {
  IL_006d:  stloc.3
  IL_006e:  ldarg.0
  IL_006f:  ldc.i4.s  -2
  IL_0071:  stfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_0076:  ldarg.0
  IL_0077:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_007c:  ldloc.3
  IL_007d:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetException(class [System.Runtime]System.Exception)
  IL_0082:  nop
  IL_0083:  leave.s  IL_0099

  }  // end handler
  IL_0085:  ldarg.0
  IL_0086:  ldc.i4.s  -2
  IL_0088:  stfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_008d:  ldarg.0
  IL_008e:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_0093:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetResult()
  IL_0098:  nop
  IL_0099:  ret
  } // end of method '<Main>d__0'::MoveNext

  .method  private hidebysig newslot virtual final
  instance void  SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine) cil managed
  // SIG: 20 01 01 12 59
  {
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerHiddenAttribute::.ctor()  = ( 01 00 00 00 )
  .override [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine
  // Method begins at RVA 0x227c
  // Code size  1 (0x1)
  .maxstack  8
  IL_0000:  ret
  } // end of method '<Main>d__0'::SetStateMachine

  } // end of class '<Main>d__0'

  .method  public hidebysig static
  class [System.Runtime]System.Threading.Tasks.Task
  Main() cil managed
  // SIG: 00 00 12 4D
  {
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type)  = ( 01 00 12 50 72 6F 67 72 61 6D 2B 3C 4D 61 69 6E  // ...Program+<Main
  3E 64 5F 5F 30 00 00 )  // >d__0..
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()  = ( 01 00 00 00 )
  // Method begins at RVA 0x2050
  // Code size  49 (0x31)
  .maxstack  2
  .locals  init (class Program/'<Main>d__0' V_0)
  IL_0000:  newobj  instance void Program/'<Main>d__0'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
  IL_000c:  stfld  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld  int32 Program/'<Main>d__0'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_001e:  ldloca.s  V_0
  IL_0020:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class Program/'<Main>d__0'>(!!0&)
  IL_0025:  ldloc.0
  IL_0026:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<Main>d__0'::'<>t__builder'
  IL_002b:  call  instance class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
  IL_0030:  ret
  } // end of method Program::Main

  .method  private hidebysig static
  class [System.Runtime]System.Threading.Tasks.Task
  DoTaskAsynchronously() cil managed
  // SIG: 00 00 12 4D
  {
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type)  = ( 01 00 22 50 72 6F 67 72 61 6D 2B 3C 44 6F 54 61  // .."Program+<DoTa
  73 6B 41 73 79 6E 63 68 72 6F 6E 6F 75 73 6C 79  // skAsynchronously
  3E 64 5F 5F 31 00 00 )  // >d__1..
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()  = ( 01 00 00 00 )
  // Method begins at RVA 0x2090
  // Code size  49 (0x31)
  .maxstack  2
  .locals  init (class Program/'<DoTaskAsynchronously>d__1' V_0)
  IL_0000:  newobj  instance void Program/'<DoTaskAsynchronously>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
  IL_000c:  stfld  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld  int32 Program/'<DoTaskAsynchronously>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_001e:  ldloca.s  V_0
  IL_0020:  call  instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class Program/'<DoTaskAsynchronously>d__1'>(!!0&)
  IL_0025:  ldloc.0
  IL_0026:  ldflda  valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program/'<DoTaskAsynchronously>d__1'::'<>t__builder'
  IL_002b:  call  instance class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
  IL_0030:  ret
  } // end of method Program::DoTaskAsynchronously

  .method  public hidebysig specialname rtspecialname
  instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
  // Method begins at RVA 0x20cd
  // Code size  8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call  instance void [System.Runtime]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  ret
  } // end of method Program::.ctor

  .method  private hidebysig specialname static
  void  '<Main>'() cil managed
  // SIG: 00 00 01
  {
  .entrypoint
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()  = ( 01 00 00 00 )
  // Method begins at RVA 0x20d8
  // Code size  19 (0x13)
  .maxstack  1
  .locals  init (valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_0)
  IL_0000:  call  class [System.Runtime]System.Threading.Tasks.Task Program::Main()
  IL_0005:  callvirt  instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
  IL_000a:  stloc.0
  IL_000b:  ldloca.s  V_0
  IL_000d:  call  instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
  IL_0012:  ret
  } // end of method Program::'<Main>'

} // end of class Program
Full IL — .NET 11 (SDK 11, runtime-async=on)
// ildasm — async-dotnet.dll (.NET 11, SDK 11, runtime-async=on)
// Metadata version: v4.0.30319
// =============== CLASS MEMBERS DECLARATION ===================

.class  public auto ansi beforefieldinit Program
  extends [System.Runtime]System.Object
{
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8)  = ( 01 00 01 00 00 )
  .custom  instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8)  = ( 01 00 00 00 00 )
  .method  public hidebysig static
  class [System.Runtime]System.Threading.Tasks.Task
  Main() cil managed
  // SIG: 00 00 12 41
  {
  // Method begins at RVA 0x2050
  // Code size  13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  call  class [System.Runtime]System.Threading.Tasks.Task Program::DoTaskAsynchronously()
  IL_0006:  call  void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
  IL_000b:  nop
  IL_000c:  ret
  } // end of method Program::Main

  .method  private hidebysig static
  class [System.Runtime]System.Threading.Tasks.Task
  DoTaskAsynchronously() cil managed
  // SIG: 00 00 12 41
  {
  // Method begins at RVA 0x205e
  // Code size  15 (0xf)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.s  100
  IL_0003:  call  class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Delay(int32)
  IL_0008:  call  void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
  IL_000d:  nop
  IL_000e:  ret
  } // end of method Program::DoTaskAsynchronously

  .method  public hidebysig specialname rtspecialname
  instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
  // Method begins at RVA 0x206e
  // Code size  8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call  instance void [System.Runtime]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  ret
  } // end of method Program::.ctor

  .method  private hidebysig specialname static
  void  '<Main>'() cil managed
  // SIG: 00 00 01
  {
  .entrypoint
  .custom  instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()  = ( 01 00 00 00 )
  // Method begins at RVA 0x2077
  // Code size  11 (0xb)
  .maxstack  8
  IL_0000:  call  class [System.Runtime]System.Threading.Tasks.Task Program::Main()
  IL_0005:  call  void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::HandleAsyncEntryPoint(class [System.Runtime]System.Threading.Tasks.Task)
  IL_000a:  ret
  } // end of method Program::'<Main>'

} // end of class Program