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.- The first variable <> 1_state stores the number of the await statement reached. No await has been encountered yet, the value of this variable is -1. All await statements in the original method are numbered, and at the moment of suspension, the state number is entered into the await number, after which it will be necessary to resume execution.
- The following variable <one> 5_1 is used to store the original variable one. In the code generated by the compiler, all references to this variable are replaced with references to this member variable.
- Next comes the variable <> 4_this . It is found only in state machines for non-static asynchronous methods and contains the object on whose behalf it was called. In a sense, this is just another local method variable, only it is used to access other variable members of the same object. In the process of converting the async method, it must be saved and used explicitly, because the code of the original object is transferred to the structure of the state machine.
- AsyncTaskMethodBuilder (<> t__builder) - represents the builder for asynchronous methods that return a task. This auxiliary type and its members are intended for use by the compiler. Here logic is encapsulated, common to all state machines. It is this type that creates the Task object returned by the stub. In fact, this type is very similar to the TaskCompletionSource class in the sense that it creates a puppet task that can be completed later. The difference from TaskCompletionSource is that AsyncTaskMethodBuilder is optimized for async methods and for the sake of performance improvement is a structure, not a class.
- TaskAwaiter (<> u_awaiter) - a temporary object is stored here that waits for the completion of an asynchronous task. It is also presented as a structure and helps the await operator to subscribe to the Task completion notification.
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 )
- localsinit is a flag that is set for a method and is used to initialize local instances of value types. It is defined in the method header and means that the variables must be initialized in CIL. All instances that will be used in this method are defined here, and they are defined as an array and are set by default, as is customary: NULL value for object types and for value type fields containing objects, 0 for integer types, and 0.0 for types with floating point.
Thus, upon entering the method, we already have ready local variables for their use in the method. - valuetype - means that it is a significant type, i.e. structure
IL_0000: ldarg.0 IL_0001: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state' IL_0006: stloc.0
- ldarg.0 - loads argument 0 to the stack. An argument with index 0 is loaded onto the calculation stack (the intermediate .NET language is a stack) copied from the input argument. But we have no arguments in the method! The fact is that by default in non-static methods, the argument with index 0 is always a pointer to an instance of the class β this. If you have arguments, they will already have an index of 1, 2, etc. In a static method, your arguments will start counting from 0.
- ldfld - searches for the value of a field in an object referenced in the stack. And the link itself was loaded higher with ldarg.0, while the value that was stored in this field, respectively, was loaded onto the stack.
- stloc.0 - retrieves the top value in the stack (this is the value of the field of the MethodTaskAsync.state object) and stores it in the list of local variables with index 0. And the list of local variables was also declared in localsinit. Convenient, isn't it?
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
- ldloc.0 and brfalse.s - loads a local variable with index 0 onto the calculation stack. Here, the state value just saved is loaded onto the stack, and the brfalse.s command transfers control to the final instruction if the state value is false, i.e. 0. When you first enter the method, the value is -1, which means that the flow of instructions goes on.
- br.s IL_000e - unconditional transmission of the final instruction. This will jump to another part of the code that needs to be executed. In this case, the next command will be executed on the IL_000e line.
- br.s IL_0054 is also an unconditional jump, only this command will be executed if the brfalse.s command is executed
- nop - Fills the space if the opcodes contain corrections. No significant operations are performed, although the processing cycle can be completed.
- ldarg.0 and ldc.i4.s 33 - here the this pointer is loaded, and the number 33 is loaded onto the stack, where ldc.i4.s - pushes the value with the Int8 type onto the calculation stack as Int32 (short form).
- stfld - replaces the value in the field of the object, by reference to the object with a new value. Using the loaded pointer and the number 33 on the stack, the member variable <one> 5_1 (initialized by default 0) loads and stores the new value - 33. As we can see, this is the first line of our original method. It is in this block that the code of the original method is executed.
- ldc.i4 1000 - load a variable with Int32 type as Int32 into the stack.
- call class [mscorlib] System.Threading.Tasks.Task [mscorlib] System.Threading.Tasks.Task :: Delay (int32) - the method is called here. A feature of this instruction (as compared to the callvirt instruction) is that the address of the called method is calculated statically, that is, even during JIT compilation. In this case, the Delay method is static. In this case, the parameters of the called method must be located in the stack from left to right, that is, first the first parameter must be loaded onto the stack, then the second, and so on.
- callvirt instance valuetype [mscorlib] System.Runtime.CompilerServices.TaskAwaiter [mscorlib] System.Threading.Tasks.Task :: GetAwaiter () - this instruction differs from call mainly because the address of the called method is determined during program execution by analyzing the type the object for which the method is being called. This implements the idea of ββlate binding, necessary to support polymorphism. In this case, the return value (in this case TaskAwaiter) is pushed onto the stack, where TaskAwaiter represents an object that is waiting for the asynchronous task to complete.
- stloc.2 - retrieves the top value in the stack and stores it in the list of local variables with index 2. It should be noted that the top value on the stack is the result of the GetAwaiter () operation and, accordingly, this value is stored in a local variable with index 2
- ldloca.s 2 - loads a local value with index 2 onto the stack - newly saved value
- call instance bool [mscorlib] System.Runtime.CompilerServices.TaskAwaiter :: get_IsCompleted () - a load on the stack of a value indicating whether the task was completed at the time the property was called: true or false
- brtrue.s IL_0070 - if the task is completed, go to the execution of another part of the code, if not, go ahead.
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();
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.
- This piece of code is responsible for changing the state variable to 0, where the stfld int32 Asynchronous.Program / 'd__0' :: '<> 1__state' command will
repeat means changing the field value to a new value. And to resume from the right place, you need to change the state variable.
- Then, the TaskAwaiter object is used to subscribe to the Task completion notification. This happens when a local variable with index 2 is loaded onto the stack and the field value is changed to the value of this local variable ( ldloc.2 and stfld valuetype [mscorlib] System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program / 'd__0' :: '<> command u__1 ' ). , .
- AwaitUnsafeOnCompleted . await, , . awaiter. : AsyncTaskMethodBuilder.AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) . 2 3, 2 β valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, 3 β class Asynchronous.Program/'d__0'
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:- s_defaultResultTask = AsyncTaskCache.CreateCacheableTask (default (TResult)) - a task is created here without implementing Dispose with the special flag DoNotDispose . This approach is used when creating tasks for caching or reuse.
- AsyncMethodBuilderCore m_coreState - represents the state associated with the execution of IAsyncStateMachine. This is a structure.
- AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize β MoveNext . , , MoveNext.
- m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn? this.Task: null, ref runnerToInitialize) β Action UnsafeOnCompleted . .
- awaiter.UnsafeOnCompleted (continuation) - plans to continue the actions that will be called when the instance completes execution. In this case, depending on whether we need to restore the context or not, the MoveNext method will be called, respectively, with the context and method suspension, or execution will continue in the context of the thread in which the task was executed.
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();
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
- In the first part, the argument with the index 0 is loaded - this is this, then the search for the TaskAwaiter <> u__1 variable is stored, its value is stored in a local variable with the index 2, and then reinitialized. After this, the value -1 is loaded onto the stack and the value is stored in the 1__state variable . The task state is reset in this way.
- In the second part, the local variable awaiter is loaded onto the stack and GetResult () is called. Then a new load on the stack of a local variable and its new initialization. Then the variable is loaded onto the stack 5__1 and stored in a local variable with index 1 and the transition to another command.
- In the third part, loading the stack with the value -2 and saving it in the variable 1_state. Then load the t_builder variable onto the stack and call the SetResult (one) method.
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();
, , 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 ).