Async / await and implementation mechanism in C # 5.0

    Details on Asynchronous Code Conversion by the Compiler


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

    Stub method


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

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

    Interesting, isn't it? The async keyword has no effect on how the method is used externally. This is noticeable because 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 overriding virtual methods, implementing an interface, or calling it.

    The only purpose of the async keyword is to change the compilation method of 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.

    State machine structure


    In the above example, 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. IL code also contains a stub method (MethodTaskAsync) called in the 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 saving the state of the method at the moment when the program reaches await. After all, as you know, the code before this keyword is executed in the calling thread, and then when it is reached, information is stored about where the program was located, so that when the program resumes, it can continue to run.

    The compiler could have acted differently: just save all the method variables. But in that case it would be necessary to generate a lot of code. However, it is possible to do otherwise, namely, simply create an instance of some type and save all the method data as members of this object. Then, when saving this object, all local method variables will be automatically saved. This is what the formed structure called the Finite State Machine is designed for.

    In short, a state machine is an abstract machine, the number of possible internal states of which is finite. Roughly speaking, the state machine, through the eyes of the 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 state machines are very efficient. Moreover, there is a finite set of input characters from which output words are formed. It should also be borne in mind that each input symbol transfers the machine to a new state. In our case, the input state will be the state of the asynchronous operation and, based on this value, the state machine will form a certain state and, accordingly, the reaction to the task (output word).

    The state machine is formed in the form of a class and contains the following member variables:
        public int32 '<>1__state';
        private int32 '5__1';
        public Mechanism_async.Program '<>4__this';
        public System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 '<>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 a correct C # program variable names cannot contain angle brackets.

    • The first variable <> 1_state stores the number of the await statement reached. Until no await is encountered, the value of this variable is -1. All await operators in the original method are numbered, and at the moment of suspension, the await number is entered into the state variable, after which it will be necessary to resume execution.
    • Next variable 5_1 serves to store the original variable one. In the code generated by the compiler, all calls to this variable are replaced by a call to this member variable.
    • Then the variable <> 4_this is found . It is found only in state machines for non-static asynchronous methods and contains the object on behalf of which it was called. In a way, 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 stored and used explicitly, because the code of the original object is transferred to the structure of the state machine.
    • AsyncTaskMethodBuilder (<> t__builder) - represents a builder for asynchronous methods that return a task. This helper type and its members are intended to be used by the compiler. This encapsulates the logic 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 improving performance it is a structure, not a class.
    • TaskAwaiter (<> u_awaiter) - this is where the temporary object that waits for the completion of the asynchronous task is stored. Also presented as a structure, it helps the await operator sign up for a notification that a Task has completed.

    For a more detailed study of what really happens under the hood of the compiler, consider the IL code generated by the compiler for d__0:
    IL code
    .class nested private auto ansi sealed beforefieldinit '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 '<>t__builder'
    	.field public class Asynchronous.Program '<>4__this'
    	.field private int32 '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 '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/'d__0',
    			[4] class [mscorlib]System.Exception
    		)
    		IL_0000: ldarg.0
    		IL_0001: ldfld int32 Asynchronous.Program/'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/'d__0'::'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/'d__0'::'<>1__state'
    			IL_0039: ldarg.0
    			IL_003a: ldloc.2
    			IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'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 Asynchronous.Program/'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::AwaitUnsafeOnCompletedd__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/'d__0'::'<>u__1'
    			IL_005a: stloc.2
    			IL_005b: ldarg.0
    			IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'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/'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/'d__0'::'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/'d__0'::'<>1__state'
    			IL_0093: ldarg.0
    			IL_0094: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 Asynchronous.Program/'d__0'::'<>t__builder'
    			IL_0099: ldloc.s 4
    			IL_009b: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::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/'d__0'::'<>1__state'
    		IL_00ab: ldarg.0
    		IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 Asynchronous.Program/'d__0'::'<>t__builder'
    		IL_00b1: ldloc.1
    		IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::SetResult(!0)
    		IL_00b7: nop
    		IL_00b8: ret
    	} // end of method '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 'd__0'::SetStateMachine
    } // end of class d__0


    MoveNext Method


    The class that was generated for the MethodTask type implements the IAsyncStateMachine interface, which represents state machines created for asynchronous methods. This type is for compiler use only. 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 at the first entrance to the method and after await. It is believed that any state machine starts its work in some initial state. Even with the simplest async method, the MoveNext code is surprisingly complex, so I’ll try to describe it and present it as accurately as possible in the C # equivalent.

    The MoveNext method is named 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 the IEnumerable interface in a single method using the yield return keyword. The finite state machine used for this purpose in many ways resembles an asynchronous state machine, only easier.

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

    • localsinit is a flag that is set for a method and serves to initialize local instances of value types. It is defined in the method header and means that the variables must be initialized in CIL. Here, all instances that will be used in this method are defined, and they are defined as an array and are set by default, as is customary: NULL 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-made 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/'d__0'::'<>1__state'
    		IL_0006: stloc.0

    • ldarg.0 - loads argument 0 onto the stack. An argument with index 0 is loaded onto the computation stack (the .NET intermediate language is a stack language) copied from the incoming argument. But we have no arguments in the method! The fact is that by default in non-static methods, an argument with index 0 is always a pointer to an instance of the class - this. If at the same time you have arguments, then 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 field value in the object, the link to which is on the stack. And the link itself was loaded above using ldarg.0, while the value that was stored in this field was accordingly loaded onto the stack.
    • stloc.0 - retrieves the top value in the stack (this is the value of the method field of the MethodTaskAsync.state object) and saves 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/'d__0'::'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 computation stack. Here, the state 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. The first time you enter the method, the value is -1, which means that the execution of the instruction stream goes further.
    • br.s IL_000e - unconditional transfer of the final instruction. This will take you to another part of the code that needs to be executed. In this case, the next command will be executed on the line IL_000e.
    • br.s IL_0054 - also an unconditional jump, only this command will be executed if the brfalse.s command is executed
    • nop - Fills the space if the operation codes contain corrections. No significant operations are performed, although a processing cycle can be completed.
    • ldarg.0 and ldc.i4.s 33 - here the this pointer is loaded, as well as the number 33 is loaded onto the stack, where ldc.i4.s - pushes a value of type Int8 onto the calculation stack as Int32 (short form of writing).
    • stfld - replaces the value in the field of the object, by reference to the object with the new value. Using a loaded pointer and number 33 on the stack, into a member variable5_1 (initialized by default 0) loads and stores a 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 - loading a variable with type Int32 as Int32 onto the stack.
    • call class [mscorlib] System.Threading.Tasks.Task [mscorlib] System.Threading.Tasks.Task :: Delay (int32) - the method is called here. The peculiarity of this instruction (in comparison with 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 on the stack from left to right, that is, first the first parameter must be loaded onto the stack, then the second, etc.
    • callvirt instance valuetype [mscorlib] System.Runtime.CompilerServices.TaskAwaiter [mscorlib] System.Threading.Tasks.Task :: GetAwaiter () - this instruction differs from call mainly in that the address of the called method is determined during program execution by type analysis 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 completion of the asynchronous task.
    • stloc.2 - retrieves the top value on the stack and saves 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 the local variable with index 2
    • ldloca.s 2 - loads a local value with index 2 onto the stack - recently saved value
    • call instance bool [mscorlib] System.Runtime.CompilerServices.TaskAwaiter :: get_IsCompleted () - loading a value on the stack that indicates whether the task was completed at the time the property was accessed: true or false
    • brtrue.s IL_0070 - if the task is completed, go to the execution of another piece of code, if not, go ahead.

    Thus, you can imagine 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 completion of the asynchronous task and goes to the right place in the method. In this case, a transition to one of several states of the automaton occurs: the method is suspended at the await meeting place or synchronous termination.

    Pause method


    Consider the IL code at the method suspension location:
                            IL_0030: ldarg.0
    			IL_0031: ldc.i4.0
    			IL_0032: dup
    			IL_0033: stloc.0
    			IL_0034: stfld int32 Asynchronous.Program/'d__0'::'<>1__state'
    			IL_0039: ldarg.0
    			IL_003a: ldloc.2
    			IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'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 Asynchronous.Program/'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::AwaitUnsafeOnCompletedd__0'>(!!0&, !!1&)
    			IL_0051: nop
    			IL_0052: leave.s IL_00b8
    

    It is not worth describing each operation, as everything is described above.
    • This piece of code is responsible for changing the state variable to 0, where the command stfld int32 Asynchronous.Program / 'd__0' :: '<> 1__state' I 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 notification about the completion of the Task task . This happens when a local variable with index 2 is loaded onto the stack and the field value changes to the value of this local variable ( ldloc.2 and stfld valuetype [mscorlib] System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program / 'd__0' :: :: <> commands u__1 ' ). Then control is returned and the thread is freed for other things, as befits a decent asynchronous method.
    • The AwaitUnsafeOnCompleted object also participates in the notification subscription procedure . This is where additional await features are implemented, including remembering the synchronization context that will need to be restored when resuming. This method schedules the state machine to proceed to the next action when the specified object of type awaiter completes execution. As parameters: AsyncTaskMethodBuilder.AwaitOnCompleted (ref TAwaiter awaiter, ref TStateMachine stateMachine) . As you can see, before calling this method, two variables are loaded onto the stack with index 2 and 3, where 2 is valuetype [mscorlib] System.Runtime.CompilerServices.TaskAwaiter, 3 is class Asynchronous.Program / 'd__0'

    Let's take a closer look at the AsyncTaskMethodBuilder structure (I won’t dig much here, because in my opinion, studying this structure and everything connected with it can be described in several articles):
             /// Кэшированная задача для default(TResult).
            internal readonly static Task s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult));
            /// Состояние, связанное с IAsyncStateMachine.
            private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly
            /// Ленивая инициализация задачи
            private Task m_task; // lazily-initialized: must not be readonly
            /// 
            /// Планирует состояние данной машины для дальнейшего действия, когда awaiter выполнится /// 
            /// Определяет тип awaiter.
            /// Определяет тип состояния машины.
            /// The awaiter.
            /// Состояние машины.
            [SecuritySafeCritical]
            public void AwaitUnsafeOnCompleted(
                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)) - this creates a task without implementing Dispose when specifying a special DoNotDispose flag . 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 the structure.
    • AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize - provides the ability to call the MoveNext state machine method according to the provided execution context of the program. This is a structure that contains the execution context, state of the state machine, and the method for executing MoveNext.
    • m_coreState.GetCompletionAction (AsyncCausalityTracer.LoggingOn? this.Task: null, ref runnerToInitialize) - gets Action when waiting for the UnsafeOnCompleted method and attaches as a continuation task. It also remembers the state of the machine and the execution context.
    • awaiter.UnsafeOnCompleted (continuation) - schedules the continuation of actions that will be called when the instance completes execution. Moreover, depending on whether we need to restore the context or not, the MoveNext method will be called accordingly with the context and pause of the method, 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(); // стоит отметить, что это локальные переменные метода, а не самого типа (исходя из IL-кода)
             if(!awaiter.IsCompleted)
             {
                this.1_state = 0;
                this.u__awaiter = awaiter; //u__awaiter это тип TaskAwaiter
                t_builder.AwaitUnsafeOnCompleted(ref this.u_awaiter, ref d__0);
                return;
             }
       }
       ...
          //рассмотрим далее
    }
    

    Method Resume


    After executing this piece of code, the calling thread leaves to go about its own business, while for now, 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 gets called when continuing:
                            IL_0054: ldarg.0
    			IL_0055: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'d__0'::'<>u__1'
    			IL_005a: stloc.2
    			IL_005b: ldarg.0
    			IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'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/'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/'d__0'::'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/'d__0'::'<>1__state'
    		        IL_00ab: ldarg.0
    		        IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 Asynchronous.Program/'d__0'::'<>t__builder'
    		        IL_00b1: ldloc.1
    		        IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::SetResult(!0)
    		        IL_00b7: nop
    

    • In the first part, the argument with index 0 is loaded - this is this, then the TaskAwaiter <> u__1 variable is searched, its value is stored in a local variable with index 2, and then reinitialized. After that, the value -1 is loaded onto the stack and this value is saved in the 1__state variable . Thus, the task state is reset.
    • In the second part, the local variable awaiter is loaded onto the stack and the GetResult () call is made . Then a new loading onto the stack of the local variable and its new initialization. Then, the variable 5__1 is loaded onto the stack and saved in a local variable with index 1 and a transition to another command takes place .
    • In the third part, loading -2 into the stack and storing it in the 1_state variable. Then loading the t_builder variable onto the stack and calling the SetResult (one) method.

    The result is an 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 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.5_1;
              this.1_state = -2;
              this.t_builder.SetResult(one);
       }
    }
    

    Synchronous completion


    In case of synchronous termination, you should 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 using the goto case statement:
    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;
              ...
       }
    }
    

    The code compiled by the compiler is good in that no one should accompany it, so you can use goto as much as you like.

    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 (you can read it in one day if you wish) and it is very interesting and moderately detailed describes the async / await mechanism as a whole. At the same time, you can read to beginners, everything is written very simply there (examples from life and the like). While reading it and studying IL code in parallel, I found a slight discrepancy with what is written in the book and is actually there. But I think that most likely the fact is that most likely the compiler has since corrected a little and it began to produce slightly different results. But this is not so critical as to get hung up on it. At the same time, as the source code (to describe AsyncTaskMethodBuilder, I used this resource:this is if anyone will be interested to dig even deeper ).

    Also popular now: