欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C# Async/Await原理剖析

程序员文章站 2022-05-08 11:29:50
...

什么是Async/Await

Async/Await是C# 5引入的关键字,用以提高用户界面响应能力和对Web资源的访问能力,同时它使异步代码的编写变得更加容易。

为什么需要Async/Await

1. 需要使用异步编程技术来提高程序的响应能力
在Windows桌面应用中,当click download button时,我们不希望其导致整个ui失去响应。我们希望ui线程能够实时的响应窗体事件,而不会被下载等耗时的IO操作所阻塞。这就需要我们使用异步编程技术来编写程序, 使其不阻塞当前线程,从而提高程序的响应能力。

2. 需要一种足够简单且易于理解的方式编写异步代码
在没有async/await的时代我们是如何实现异步的?下面的代码还是以Windows桌面应用为????,当click download button 时,应用发送网络请求获取project数据并show在windows窗体中。

private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
    ...
    Task<Project> task = projectService.GetAsync(id);
    
    task.ContinueWith((t) => {
        var project = t.Result;
        ProjectBox.Items.Add(project);
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

TaskScheduler.FromCurrentSynchronizationContext()表示回调代码将会在当前同步上下文中执行,如果去掉此参数,可能导致程序抛出异常。原因是将回调代码安排到线程池中的非UI线程时,它无法访问UI控件。

更多内容:ExecutionContext vs SynchronizationContext

使用回调函数的方式编写单个异步操作,似乎看不出来有什么不妥。但是随着异步操作的增加,我们就会遇到下面这样的代码。????????????

private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
    ...
    task1.ContinueWith((t) => {
        ...
        task2.ContinueWith(t =>
        {
            ...
            task3.ContinueWith(t =>
            {
                ...
               	task4.ContinueWith(t =>
                {
					...
                });
            });
        });
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

上面的代码可能是很多人对异步望而生畏的原因,由于其功能逻辑代码嵌套的层次太多,导致可读性降低,维护困难。

对于上述被称之为回调地狱的问题,async/await提供了一种简单且易于理解的方式来编写异步操作,其代码如下所示。????????????

private async void DownloadButton_Click(object sender, RoutedEventArgs e)
{
    ...
    Project project = await projectService.GetAsync(id);
    ProjectBox.Items.Add(project);
}

Async/Await是如何实现的

下面这张图片,来自microsoft 的一篇文档 Task asynchronous programming model,它描述了在异步编程中控制流是如何在方法之间移动的。
C# Async/Await原理剖析
上图很清晰的描述出了AccessTheWebAsync方法内部代码的执行过程,那么疑问????️是:Async/Await是如何实现暂停后续代码的执行,将控制权交还给调用者,而在异步操作完成时继续执行后续代码的?

AsyncStateMachine:
为了解开疑团,我将下面的代码进行了反编译。

public async Task<ProjectVo> GetAsync(long projectId) 
{
    Project resultOfAwaiter1 = await projectRepo.GetAsync(projectId);
    List<Person> resultOfAwaiter2 = await personRepo.GetAsync(project.getMembers());
    ProjectVo result = ToProjectVo(resultOfAwaiter1, resultOfAwaiter2);
    return result;
}

下面是使用dotPeek反编译后得到的代码,为了便于理解我将得到的代码进行了一些调整。(主要是重命名和去除一些无益于理解其原理的代码)

从代码中我们可以看到在编译后async和await关键字不见了,取而代之的是:编译器为我们生成了实现IAsyncStateMachine接口的内部类GetAsync_StateMachine,并在GetAsync方法体内setup并初始化它的实例。通过stateMachine.builder.Start来启动状态机,并在最后返回一个新的task。

[AsyncStateMachine(typeof (ProjectService.GetAsync_StateMachine))]
public Task<ProjectVo> GetAsync(long projectId)
{
    MainWindow.AwaitButtonClick_StateMachine stateMachine = new MainWindow.AwaitButtonClick_StateMachine();
    stateMachine.caller = this;
    stateMachine.projectId = projectId;
    stateMachine.builder = AsyncTaskMethodBuilder<ProjectVo>.Create();
    stateMachine.state = -1;
    // Start方法内部执行 -> stateMachine.MoveNext()
    stateMachine.builder.Start<ProjectService.GetAsync_StateMachine>(ref stateMachine);
    // 返回一个新的task(可能完成也可能未完成)
    return stateMachine.builder.Task;
}

[CompilerGenerated]
private sealed class GetAsync_StateMachine : IAsyncStateMachine
{
    public int state;
    public AsyncTaskMethodBuilder<ProjectVo> builder;
    public ProjectService caller;
    // 原函数的传入参数
    public long projectId;
    // 原函数的局部变量
    private Project resultOfAwaiter1;
    private List<Person> resultOfAwaiter2;
    private ProjectVo result;
    private TaskAwaiter<Project> awaiter1;
    private TaskAwaiter<List<Person>> awaiter2;

    void IAsyncStateMachine.MoveNext()
    {
        try
        {
            switch (this.state)
            {
                case 0:
                    this.state = -1;
                    break;
                case 1:
                    this.state = -1;
                    goto label_8;
                case -1:
                    // 开始第一个Task并获得awaiter,通过awaiter来观察Task是否完成。
                    this.awaiter1 = this.caller.projectRepo.GetAsync(this.projectId)
                        .GetAwaiter();
                    if (!this.awaiter1.IsCompleted)
                    {
                        this.state = 0;   
                        // 向未完成的Task中注册continuation action;
                        // continuation action会在Task完成时执行;
                        // 等同于awaiter1.onCompleted(() => this.MoveNext());
                        this.builder.AwaitUnsafeOnCompleted<TaskAwaiter<Project>, ProjectService.GetAsync_StateMachine>(ref this.awaiter1, ref this);
                        // return(即交出控制权给GetAsync的调用者)
                        return;
                    }
                    break;
            }
            // 第一个Task完成,获取结果
            this.resultOfAwaiter1 = this.awaiter1.GetResult();
            // 开始第二个Task
            this.awaiter2 = this.caller.personRepo
                .GetAsync(resultOfAwaiter1.getMembers())
                .GetAwaiter();
            if (!this.awaiter2.IsCompleted)
            {
                this.state = 1;
                // 向未完成的Task中注册continuation action
                this.builder.AwaitUnsafeOnCompleted<TaskAwaiter<List<Person>>, ProjectService.GetAsync_StateMachine>(ref this.awaiter2, ref this);
                // return
                return;
            }
label_8: // 标记,用于goto跳转
            // 第二个Task完成,获取结果
            this.resultOfAwaiter2 = this.awaiter2.GetResult();
            
            this.result = this.caller.ToProjectVo(this.resultOfAwaiter1, this.resultOfAwaiter2);
        }
        catch (Exception ex)
        {
            this.state = -2;
            this.builder.SetException(ex);
            return;
        }
        this.state = -2;
        // 将builder标记为completed;
        // 将未完成的task标记为completed;(这里的task指GetAsync的返回值)
        // set result并run continuation;
        this.builder.SetResult(this.result);
    }
}

值得注意的是:

  1. stateMachine.builder.Start方法内部是在调用stateMachine.MoveNext
  2. stateMachine.builder.Task返回 new task, task可能已完成也可能未完成,这取决于stateMachine是否达到了最终状态即-2
  3. this.builder.AwaitUnsafeOnCompleted === awaiter1.onCompleted(() => this.MoveNext()) === task.ContinueWith(()=> this.MoveNext())
  4. this.builder.SetResult(this.result)将未完成的task标记为completed同时设置最终结果,然后执行continuation action

关于AsyncStateMachine的生命周期:
它以状态-1启动,每当await一个未完成的task时,其状态将转换为一个非负整数,直到task完成才被再次转换为-1,如此反复直到执行完所有代码(即执行到source code中的return)才以状态-2结束。
C# Async/Await原理剖析
由上我们可以得出:

  1. AsyncStateMachine内部状态的数量=n+2,n即async方法体内await出现的次数。
  2. AsyncStateMachine的状态为非负整数时,它会暂停执行并交出控制权,只有当它的状态为-1时才会继续执行。
  3. 如果足够幸运,只调用一次MoveNext就可以让AsyncStateMachine变成最终状态(-2)。

搞清楚单个state machine的运作过程后,我们来看看多个state machine是如何协作的:

下面的序列图展示了,一款Windows桌面应用在click download button时,各个方法之间的调用过程。
其主体部分为两个state machine的交互过程,即异步方法DownloadButton_ClickprojectService.GetAsync所生成的state machine之间的交互过程。其中在DownloadButton_Click方法内部调用了异步方法projectService.GetAsync,而在projectService.GetAsync方法内部则依次调用了异步方法projectRepo.GetAsyncpersonRepo.GetAsync
图中各元素含义如下:

  • sm为state machine的缩写,s为state的缩写,t为task的缩写。
  • 红绿蓝三个背景块分别表示执行过程的三个阶段,即task1未完成阶段、task1完成阶段、task3完成阶段。
  • 黄色方块为对应state machine的状态。
    C# Async/Await原理剖析

一些没有提到的细节:

  • 编译器会为标记为async的方法创建AsyncStateMachine,即便在方法体内没有使用await
  • 编译器会根据异步方法的返回值类型Task和void,为stateMachine 分别setup AsyncTaskMethodBuilder<T>AsyncVoidMethodBuilder
  • 对于async的方法内的每个局部变量和方法传入参数,编译器都在AsyncStateMachine类中为其创建对应的属性。
  • 可被await的对象需要有T GetAwaiter()方法并且其返回值T需要实现INotifyCompletion或者ICriticalNotifyCompletion接口、具有bool IsCompleted()方法和TResult GetResult()方法
  • stateMachine.builder.Start方法的内部代码中,在调用stateMachine.MoveNext()的前后位置,分别捕获、还原了ExecutionContextSynchronizationContext

不是总结的总结

什么是async/await,它为我们带来了什么?
async/await本质上是通过编译器实现的语法糖,它让我们能够轻松的写出简洁、易懂、易维护的异步代码。

学习它的原理有何益处?

  1. 可以学习它巧妙的设计思路????
  2. 避免滥用,减少代码中不必要的async/await
  3. 可以拓宽异步问题的调试思路,快速确定产生问题的原因

欢迎拍????指正

相关标签: c# 异步编程

上一篇: 打标点

下一篇: 大学生的集训