C# Async/Await原理剖析

什么是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,它描述了在异步编程中控制流是如何在方法之间移动的。
navigation-trace-async-program
上图很清晰的描述出了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结束。
StateMachine
由上我们可以得出:

  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的状态。
    Muti-StateMachine

一些没有提到的细节:

  • 编译器会为标记为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. 可以拓宽异步问题的调试思路,快速确定产生问题的原因

欢迎拍🧱指正

  • 25
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
async/awaitC#中用于编写异步代码的关键字。它基于任务(Task)和异步操作模式(Async Operation Pattern)来实现异步编程。当方法被标记为async时,这个方法可以包含await关键字,用于等待一个异步操作完成。在等待异步操作期间,方法会立即返回,不会阻塞线程。当异步操作完成后,方法会继续执行剩下的代码。 在编译时,编译器会将带有await关键字的代码转换为状态机(State Machine)的形式,以便正确处理异步操作的状态和结果。这样就实现了代码的简洁、易懂、易维护。 具体实现上,编译器会生成一个状态机类来管理异步操作的状态。每当遇到await关键字时,编译器会将await后面的表达式封装为一个任务(Task),并将控制权返回给调用者。当异步操作完成后,状态机会通过回调或轮询等方式,重新获取控制权并继续执行剩下的代码。 总结起来,async/await通过编译器实现的语法糖,使得编写异步代码变得更加简单和直观,同时保持了代码的可读性和可维护性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Async和Await异步编程的原理](https://blog.csdn.net/sD7O95O/article/details/116382292)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C# Async/Await原理剖析](https://blog.csdn.net/weixin_43990579/article/details/105417652)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值