• C#复习笔记(5)--C#5:简化的异步编程(异步编程的深入分析)


    首先,阐明一下标题的这个“深入分析”起得很惭愧,但是又不知道该起什么名字,这个系列也主要是做一些复习的笔记,供自己以后查阅,如果能够帮助到别人,那自然是再好不过了。

    然后,我想说的是异步方法的状态机真的是太复杂了。我写完这篇都还迷迷糊糊的,所以,读者就不要往下看了。这里面还涉及大量的核心类型没有搞清楚。

    异步编程的深入分析

    先来一下整体的概括:异步编程的实现(包括近似实现和真实编译器生成的代码)基本上可以说是一个状态机。编译器将生成一个私有的内嵌结构(现在是类了),来表示这个异步方法。编译器同时“改造了”那个异步方法,其签名与所声明的方法签名相同。我称其为骨架方法,该方法本身没有多少内容,但其他东西都依赖于它。骨架方法需要创建状态机,并执行一个步骤(此处的步骤指执行第一个await表达式之前的代码),然后返回一个表示状态机进度的任务。(别忘了,在第一次到达真正需要等待的await表达式之前,执行过程是同步的。)此后,骨架方法的运作就此结束。状态机会负责其余事项,后续操作附加到其他异步操作后,可通知状态机去执行另一个步骤。当之前返回的任务被赋予适当的值后,方法就执行到最后了,状态机可随即发出信号。下图展示了这一流程图。

    当然,“执行方法体中的代码”这一步,只有在骨架方法中第一次调用时,才会从方法的开头执行。以后每次到达该块,都是由后续操作从之前中断的地方开始继续执行。

    这个状态机表示为一个密封类(或者结构,我用ILSpy反编译后看到的是一个类),在了解细节之前,先看看编译器生成的这个状态机到底是什么样子,首先看一下“源码”示例:

     static async Task<int> SumCharactersAsync(IEnumerable<char> text)
            {
                int total = 0;
                foreach (char ch in text)
                {
                    int unicode = ch;
                    await Task.Delay(unicode);
                    total += unicode;
                }
                await Task.Yield();
                return total;
            }

     在编译器看到async和await关键字之后,就会在后台生成一个状态机类,这个类实现一个IAsyncStateMachine接口,这个接口定义如下:

    public interface IAsyncStateMachine
    {
        void MoveNext();
    
        void SetStateMachine(IAsyncStateMachine stateMachine);
    }

    状态机主要逻辑都在MoveNext里面。

    在查看生成的状态机之前,先指出以下几点:

    • 该方法包含一个参数(text)。
    • 该方法包含一个循环,后续操作执行时需跳回该循环内。
    • 该方法包含两个不同类型的await表达式:Task.Delay返回一个Task,而Task.Yield()则返回一个YieldAwaitable。而awaitable模式最重要的实现是拥有一个GetAwaiter的方法。该方法返回一个awaiter,awaiter实现ICriticalNotifyCompletion, INotifyCompletion这两个接口的核心是上下文。这两个接口中的UnsafeOnCompleted方法和OnCompleted方法最终会在AsyncTaskMethodBuilder(如果有返回值话,它是一个泛型的类)里面的AwaitUnsafeOnCompleted方法或者AwaitOnCompleted方法里面进行调用。AwaitUnsafeOnCompleted方法或者AwaitOnCompleted方法中会对ExecutionContext进行封送,ExecutinContext是所有上下文的容器,比如SynchronizationContext。ExecutionContext的Capture方法和Run方法是在附加后续操作时进行捕获上下文和执行后续操作时还原上下文的关键。这两个方法会在awaiter和AsyncTaskMethodBuilder(及其兄弟类)进行调用。
    • 该方法包含显式的局部变量(total、ch和unicode),需在不同的调用间关注其变化。
    • 该方法包含一个通过调用text.GetEnumerator()方法创建的隐式局部变量。
    • 该方法最终返回一个值。

    上面的这个“源码”会被编译器改造成骨架方法

    先来看一下这个骨架方法:

    可以看到骨架方法中首先声明了一个状态机:然后对这个状态机中的字段做了一些初始化的工作。可以看到这个状态机的名字就是用源码中方法的名字加上一些特定的字符后组成的。然后初始化了一些字段,最后,这句代码让状态机同步地执行第一个步骤,并在方法完成时或到达需等待的异步操作点时得以返回。第一个步骤完成后,骨架方法将返回builder中的任务。状态机在结束时,会使用builder来设置结果或异常。

    我们得看一下生成的这个状态机了:

     

    状态机一般是由两大部分组成:

    第一部分是字段:我将字段分成了三种类型,为了方便,我在字段的后面用小括号加注释加以说明:

    • 固定的字段,在本例中,就是<>t_builder(异步方法生成器,AsyncTaskMethodBuilder),<>u_1(awaiter),还有一个表示状态机状态的<>1_state字段。对于<>t_builder(异步方法生成器)来说,不同的返回类型的异步方法被编译器处理后有不同类型结构:如果返回void,那么<>t_builder(异步方法生成器)就是AsyncVoidMethodBuilder类型的,如果返回一个Task<T>,那么就是AsyncTaskMethodBuilder<T>,如果返回Task,就是AsyncTaskMethodBuilder;<>t_builder(异步方法生成器)具有很多功能,包括创建骨架方法返回的Task和Task<T>,即异步方法结束时传播的任务,其内包含有正确结果。对于<>u_1(awaiter)来说,是由await表达式中的可等待类型来决定的,本例中,异步方法有两个await表达式,他们的类型都不一样,一个是TaskAwaiter,另一个是YieldAwaitable.YieldAwaiter。异步方法中使用的awaiter如果是值类型,则每个类型都会有一个字段与之对应,而如果是引用类型(编译时的类型),则所有awaiter共享一个字段。本例有两个await表达式,分别使用两个不同的awaiter结构类型,因此有两个字段。如果第二个await表达式也使用了一个TaskAwaiter,或者如果TaskAwiater和YieldAwiter都是类,则只会有一个字段。由于一次只能存活一个awaiter,因此即使一次只能存储一个值也没关系。
    • ②由方法传入的参数被提升的字段,如果异步方法有参数,那么在生成的状态机中会将这些参数提升成状态机的字段,在这里是text。
    • ③方法中使用的局部变量。此例中涉及到的局部变量有total、unicode,还有一个foreach循环中调用GetEnumerator生成的局部变量。将局部变量提升为字段是由于需在多次调用MoveNext()方法时保存变量的值。

    第二部分是方法:方法都是实现了IStateMachine接口中的,一个是MoveNext,另一个是SetStateMachine。

    MoveNext方法在一开始便投入使用,并且可用于所有await表达式的后续操作。每当调用MoveNext()方法时,状态机就会通过state字段计算出方法要跳转到的位置。在准备计算结果时,则跳转到方法的逻辑起始位置或await表达式的末尾。每个状态机只执行一次操作。实际上,在方法内部存在一个基于state的switch语句,每种情况都具有包含不同标签的对应goto语句。

    本例中的MoveNext方法的代码如下:

     

    可以看到这个方法很长,看着很恶心。

    初始状态始终为-1,方法执行时状态也是-1(与等待时被暂停相反)。非负值均表示一个后续操作的目标。状态机在结束时的状态为-2。

    在方法执行过程中,在原始异步方法的return语句处,会设置result变量。然而在到达方法的逻辑末尾时,将其用于builder.SetResult()的调用。即使是非泛型的AysncTaskMethodBuilder和AsyncVoidMethodBuilder类型,也包含SetResult()方法。前者表示对于从骨架方法返回的任务来说,该方法已经完成;后者则表示原始的SynchronizationContext已经完成。(异常会以同样的方式向原始的SynchronizationContext传播。这是一种相当丑陋的跟踪方式,但却对必须使用void方法的场景提供了一种解决方案。)

    任何await表达式均表示执行路径的一个分支。首先,被等待的异步操作得到一个awaiter,然后检查其IsCompleted属性。若返回true,即可立即获得结果并继续。否则,需进行以下处理。

    • 存储awaiter,以供后面使用。
    • 更新状态,以表示从哪里继续。
    • 为awaiter附加后续操作。
    • 从MoveNext()返回,确保不会执行任何finally块。

    如果等待(await)的操作有返回值(如使用HttpClient分配awaitclient.GetStringAsync(…)的结果),那么上述代码末尾处的GetResult()调用将得到该值。类似于的代码。

    AwaitUnsafeOnCompleted方法将后续操作附加给awaiter,MoveNext()方法开头的switch语句可确保再次执行MoveNext()时,将控制传递给DemoAwaitContinuation。

    说明 AwaitOnCompleted和AwaitUnsafeOnCompleted 在此前展示的一组接口中,IAwaiter<T>扩展了INotifyCompletion及其OnCompleted方法,此外还扩展了ICriticalNotifyCompletion接口及其UnsafeOnCompleted方法。状态机为实现ICriticalNotifyCompletion的awaiter调用builder.AwaitUnsafeOnCompleted,或为只实现INotifyCompletion的awaiter调用builder.AwaitOnCompleted。后续的章节在讨论可等待模式如何与上下文交互时,会介绍这两个调用间的区别。

  • 相关阅读:
    第 9 章 用户自己建立数据类型
    第 10 章 对文件的输入输出
    第 7 章 用函数实现模块化程序设计
    第 4 章 选择结构程序设计
    第 5 章 循环结构程序设计
    第 6 章 利用数组处理批量数据
    第 3 章 最简单的 C 程序设计——顺序程序设计
    第 1 章 程序设计和 C 语言
    第 2 章 算法——程序的灵魂
    SQL(SQL Server) 批量替换两列的数据
  • 原文地址:https://www.cnblogs.com/pangjianxin/p/8710471.html
Copyright © 2020-2023  润新知