πŸ“œ ⬆️ ⬇️

Async / await and the implementation mechanism in C # 5.0

Details on the conversion of asynchronous code by the compiler.


The async mechanism is implemented in the C # compiler with support from the .NET base class libraries. There was no need to make any changes to the performing environment itself. This means that the await keyword is implemented by converting to a view that we could have written ourselves in previous versions of C #. To study the generated code, you can use the .NET Reflector or ILSpy decompiler. This is not only interesting, but also useful for debugging, performance analysis and other types of asynchronous code diagnostics.

Stub method


Consider first a simple example of an asynchronous method:
public async Task<Int32> MethodTaskAsync() { Int32 one = 33; await Task.Delay(1000); return one; } 

This example is quite simple, but practical enough and convenient for explaining the basic principle of implementing async / await. Run ILSpy and examine the code that the C # compiler automatically generates:
  [AsyncStateMachine(typeof(Program.<MethodTaskAsync>d__0))] public Task<int> MethodTaskAsync() { Program.<MethodTaskAsync>d__0 <MethodTaskAsync>d__ = new Program.<MethodTaskAsync>d__0(); <MethodTaskAsync>d__.<>4__this = this; <MethodTaskAsync>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); <MethodTask>d__.<>1__state = -1; AsyncTaskMethodBuilder<int> <>t__builder = <MethodTaskAsync>d__.<>t__builder; <>t__builder.Start<Program.<MethodTaskAsync>d__0>(ref <MethodTaskAsync>d__); return <MethodTaskAsync>d__.<>t__builder.Task; } 

Interesting, isn't it? The async keyword has no effect on how the method is used externally. This is evident by the fact that the signature of the method generated by the compiler corresponds to the original method with the exception of the word async. To some extent, the async specifier is not considered part of the method signature, for example, when it comes to redefining virtual methods, implementing an interface, or calling.

The only purpose of the async keyword is to change the method of compiling the corresponding method; it has no effect on interaction with the environment. Also note that in the "new" method there are no traces of the original code.

Structure of the state machine


In the example above, the compiler automatically applied the AsyncStateMachine attribute to the method. When a method (MethodTaskAsync) has an async modifier, the compiler generates an IL including the state machine structure.
')
This structure contains the code in the method. The IL code also contains a stub method (MethodTaskAsync) called in a state machine. The compiler adds the AsyncStateMachine attribute to the stub method so that the corresponding state machine can be identified. This is necessary in order to make an object capable of maintaining the state of the method at the moment when the program reaches await. After all, as you know, the code up to this keyword is executed in the calling thread, and then when it is reached, information is stored on where in the method the program was located so that when it resumes, the program can continue execution.

The compiler could do it differently: just save all the variables of the method. But in this case, you would have to generate a lot of code. However, you can do otherwise, namely, just create an instance of some type and save all the method data as members of this object. Then when saving this object all local variables of the method will be automatically saved. For this purpose, the intended structure, called the finite state machine (Finite State Machine), is intended.

In short, a finite state machine is an abstract automaton, the number of possible internal states of which is finite. To put it bluntly, a finite-state machine through the eyes of a user is a black box into which you can transfer something and get something from there. This is a very convenient abstraction, which allows you to hide a complex algorithm, in addition, finite automata are very effective. At the same time there is a finite set of input symbols from which the output words are formed. You should also take into account the fact that each input symbol transfers the automaton to a new state. In our case, the input characters will be the state of the asynchronous operation and, based on this value, the state machine will form some state and, accordingly, the response to the task execution (output word). This approach simplifies the formation and management of asynchronous tasks. More details about state machines can be found on the Internet with a bunch of detailed articles.

A state machine is formed as a class and contains the following member variables:
  public int32 '<>1__state'; private int32 '<one>5__1'; public Mechanism_async.Program '<>4__this'; public System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder'; private System.Runtime.CompilerServices.TaskAwaiter '<>u__awaiter'; 

The names of all variables contain angle brackets, indicating that the names are generated by the compiler. This is necessary so that the generated code does not conflict with the user code, because in the correct C # program the variable names cannot contain angle brackets.


For a more detailed study of what actually happens under the hood of the compiler, consider the IL code generated by the compiler for <MethodTaskAsync> d__0:
IL code
 .class nested private auto ansi sealed beforefieldinit '<MethodTaskAsync>d__0' extends [mscorlib]System.Object implements [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field public int32 '<>1__state' .field public valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder' .field public class Asynchronous.Program '<>4__this' .field private int32 '<one>5__1' .field private valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter '<>u__1' // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20ef // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method '<MethodTaskAsync>d__0'::.ctor .method private final hidebysig newslot virtual instance void MoveNext () cil managed { .override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext() // Method begins at RVA 0x20f8 // Code size 185 (0xb9) .maxstack 3 .locals init ( [0] int32, [1] int32, [2] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, [3] class Asynchronous.Program/'<MethodTaskAsync>d__0', [4] class [mscorlib]System.Exception ) IL_0000: ldarg.0 IL_0001: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>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_0054 IL_000e: nop IL_000f: ldarg.0 IL_0010: ldc.i4.s 33 IL_0012: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1' IL_0017: ldc.i4 1000 IL_001c: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Delay(int32) IL_0021: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter [mscorlib]System.Threading.Tasks.Task::GetAwaiter() IL_0026: stloc.2 IL_0027: ldloca.s 2 IL_0029: call instance bool [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted() IL_002e: brtrue.s IL_0070 IL_0030: ldarg.0 IL_0031: ldc.i4.0 IL_0032: dup IL_0033: stloc.0 IL_0034: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0039: ldarg.0 IL_003a: ldloc.2 IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1' IL_0040: ldarg.0 IL_0041: stloc.3 IL_0042: ldarg.0 IL_0043: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder' IL_0048: ldloca.s 2 IL_004a: ldloca.s 3 IL_004c: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, class Asynchronous.Program/'<MethodTaskAsync>d__0'>(!!0&, !!1&) IL_0051: nop IL_0052: leave.s IL_00b8 IL_0054: ldarg.0 IL_0055: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1' IL_005a: stloc.2 IL_005b: ldarg.0 IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1' IL_0061: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter IL_0067: ldarg.0 IL_0068: ldc.i4.m1 IL_0069: dup IL_006a: stloc.0 IL_006b: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0070: ldloca.s 2 IL_0072: call instance void [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult() IL_0077: nop IL_0078: ldloca.s 2 IL_007a: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter IL_0080: ldarg.0 IL_0081: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1' IL_0086: stloc.1 IL_0087: leave.s IL_00a3 } // end .try catch [mscorlib]System.Exception { IL_0089: stloc.s 4 IL_008b: ldarg.0 IL_008c: ldc.i4.s -2 IL_008e: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0093: ldarg.0 IL_0094: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder' IL_0099: ldloc.s 4 IL_009b: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetException(class [mscorlib]System.Exception) IL_00a0: nop IL_00a1: leave.s IL_00b8 } // end handler IL_00a3: ldarg.0 IL_00a4: ldc.i4.s -2 IL_00a6: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_00ab: ldarg.0 IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder' IL_00b1: ldloc.1 IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0) IL_00b7: nop IL_00b8: ret } // end of method '<MethodTaskAsync>d__0'::MoveNext .method private final hidebysig newslot virtual instance void SetStateMachine ( class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine ) cil managed { .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine) // Method begins at RVA 0x21d0 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method '<MethodTaskAsync>d__0'::SetStateMachine } // end of class <MethodTaskAsync>d__0 


MoveNext method


The class that was created for the MethodTask type implements the IAsyncStateMachine interface, which represents state machines created for asynchronous methods. This type is intended only for use by the compiler. This interface contains the following members: MoveNext and SetStateMachine. The MoveNext method moves the state machine to its next state. This method contains the original code and is called both when the method is first entered and after await. It is believed that any state machine starts its work in some initial state. Even in the case of the simplest async method, the MoveNext code is surprisingly complex, so I will try to describe it and present it as precisely as possible in C # equivalent.

The MoveNext method is named this way because of its similarity to the MoveNext methods that were generated by iterator blocks in previous versions of C #. These blocks allow you to implement an IEnumerable interface in a single method using the yield return keyword. The state machine used for this purpose is in many ways reminiscent of an asynchronous machine, only simpler.

Consider the intermediate code and analyze what happens in it (I want to note that below I decided to fully describe the CIL language for a more complete review of what the compiler generates, and also described all the instructions, so that you are not interested in technical details):
 .locals init ( [0] int32, [1] int32, [2] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, [3] class Asynchronous.Program/'<MethodTaskAsync>d__0', [4] class [mscorlib]System.Exception ) 



  IL_0000: ldarg.0 IL_0001: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0006: stloc.0 



  IL_0007: ldloc.0 IL_0008: brfalse.s IL_000c IL_000a: br.s IL_000e IL_000c: br.s IL_0054 IL_000e: nop IL_000f: ldarg.0 IL_0010: ldc.i4.s 33 IL_0012: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1' IL_0017: ldc.i4 1000 IL_001c: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Delay(int32) IL_0021: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter [mscorlib]System.Threading.Tasks.Task::GetAwaiter() IL_0026: stloc.2 IL_0027: ldloca.s 2 IL_0029: call instance bool [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted() IL_002e: brtrue.s IL_0070 


Thus, you can submit a code similar to the following:
 public void MoveNext() { switch(this.1_state) { case -1: this.one = 33; var task = Task.Delay(1000); var awaiter = task.GetAwaiter(); //  ,     ,     (  IL-) if(!awaiter.IsCompleted) { ... return; } } ... //  } 

The code presented above is responsible for the initial state of the state machine and checks the completeness of the asynchronous task and goes to the right place of the method. In this case, a transition occurs to one of several states of the automaton: the method is suspended at the await meeting point or synchronous completion.

Method suspension


Consider the IL-code in place of the suspension method:
  IL_0030: ldarg.0 IL_0031: ldc.i4.0 IL_0032: dup IL_0033: stloc.0 IL_0034: stfld int32 Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>1__state' IL_0039: ldarg.0 IL_003a: ldloc.2 IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>u__1' IL_0040: ldarg.0 IL_0041: stloc.3 IL_0042: ldarg.0 IL_0043: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>t__builder' IL_0048: ldloca.s 2 IL_004a: ldloca.s 3 IL_004c: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, class Asynchronous.Program/'<MethosTaskAsync>d__0'>(!!0&, !!1&) IL_0051: nop IL_0052: leave.s IL_00b8 

Describe each operation is not worth it, since everything is already described above.

Let's take a closer look at the AsyncTaskMethodBuilder structure (I will not dig deeply here, because in my opinion, the study of this structure and everything connected with it can be described, as it were, not for a few articles):
  /// <summary>   default(TResult).</summary> internal readonly static Task<TResult> s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)); /// <summary>,   IAsyncStateMachine.</summary> private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly /// <summary>  </summary> private Task<TResult> m_task; // lazily-initialized: must not be readonly /// <summary> ///       ,  awaiter  /// </summary> /// <typeparam name="TAwaiter">  awaiter.</typeparam> /// <typeparam name="TStateMachine">   .</typeparam> /// <param name="awaiter">The awaiter.</param> /// <param name="stateMachine"> .</param> [SecuritySafeCritical] public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { try { AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); //    await,            if (m_coreState.m_stateMachine == null) { //         var builtTask = this.Task; //      internal-, //      . m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, builtTask); } awaiter.UnsafeOnCompleted(continuation); } catch (Exception e) { AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null); } } 

Consider briefly what is inside this structure:

We get a slightly different source code:
 public void MoveNext() { switch(this.1_state) { case -1: this.one = 33; var task = Task.Delay(1000); var awaiter = task.GetAwaiter(); //  ,     ,     (  IL-) if(!awaiter.IsCompleted) { this.1_state = 0; this.u__awaiter = awaiter; //u__awaiter   TaskAwaiter t_builder.AwaitUnsafeOnCompleted(ref this.u_awaiter, ref <MethodTaskAsync>d__0); return; } } ... //  } 

Method renewal


After executing this piece of code, the calling thread leaves to go about its business, and in the meantime we are waiting for the task to complete. Once the task has been completed, the method is called again the MoveNext (with the method call AwaitUnsafeOnCompleted done everything necessary for work). Consider the IL code that is invoked during the continuation:
  IL_0054: ldarg.0 IL_0055: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1' IL_005a: stloc.2 IL_005b: ldarg.0 IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1' IL_0061: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter IL_0067: ldarg.0 IL_0068: ldc.i4.m1 IL_0069: dup IL_006a: stloc.0 IL_006b: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0070: ldloca.s 2 IL_0072: call instance void [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult() IL_0077: nop IL_0078: ldloca.s 2 IL_007a: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter IL_0080: ldarg.0 IL_0081: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1' IL_0086: stloc.1 IL_0087: leave.s IL_00a3 IL_00a3: ldarg.0 IL_00a4: ldc.i4.s -2 IL_00a6: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_00ab: ldarg.0 IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder' IL_00b1: ldloc.1 IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0) IL_00b7: nop 


As a result, the approximate source code:
 public void MoveNext() { switch(this.1_state) { case -1: this.one = 33; var task = Task.Delay(1000); var awaiter = task.GetAwaiter(); //  ,     ,     (  IL-) if(!awaiter.IsCompleted) { this.1_state = 0; this.u__awaiter = awaiter; //u__awaiter   TaskAwaiter t_builder.AwaitUnsafeOnCompleted(ref this.u_awaiter, ref <MethodTaskAsync>d__0); return; } case 0: var awaiter = this.u_awaiter; this.u_awaiter = new System.Runtime.CompilerServices.TaskAwaiter(); this.1_state = -1; awaiter.GetResult(); awaiter = new System.Runtime.CompilerServices.TaskAwaiter(); var one = this.<one>5_1; this.1_state = -2; this.t_builder.SetResult(one); } } 

Synchronous completion


In the case of synchronous completion, do not stop and resume the method. In this case, you just need to check the execution of the method and go to the right place with the help of the goto case operator:
 public void MoveNext() { switch(this.1_state) { case -1: this.one = 33; var task = Task.Delay(1000); var awaiter = task.GetAwaiter(); //  ,     ,     (  IL-) if(awaiter.IsCompleted) { goto case 0; } case 0: this.1_state = 0; ... } } 

, , goto .

And finally ...


In this article, I relied on one of my favorite books on asynchronous programming in C # 5.0 by Alex Davis. In general, I advise everyone to read it, because it is small (if you wish, you can read it in one day) and very interestingly and fairly describes in detail the async / await mechanism as a whole. In this case, you can read and beginners, everything is very simply written (examples from life and the like). While reading it and studying the IL-code in parallel, I found a slight discrepancy with what is written in the book and there really is. But I think that the most likely thing is that most likely since then a little bit has changed the compiler and it began to produce slightly different results. But it is not so critical to get hung up on it. At the same time as the source code (to describe AsyncTaskMethodBuilder used this resource:This is if anyone would be interested to dig even deeper ).

Source: https://habr.com/ru/post/260217/


All Articles