async/await 的基本实现和 .NET Core 2.1 中相关性能提高

前言

这篇文章的开头,笔者想多说两句,不过也是为了之后不再多嘴这样的话。git

在平常工做中,笔者接触得最多的开发工做仍然是在 .NET Core 平台上,固然由于团队领导的开放性和团队风格的多样性(这和 CTO 以及主管的我的能力也是分不开的),业界前沿的技术概念也都能在上手的项目中出现。因此虽然如今团队仍然处于疾速的发展中,也存在一些奇奇怪怪的事情,工做内容也算有紧有松,可是整体来讲也算有苦有乐,不是十分排斥。github

其实这样的环境有些相似于笔者心中的“圣地” Thoughtworks 的 雏形(TW的HR快来找我啊),笔者和女友谈到本身最想作的工做也是技术咨询。此类技术咨询公司的开发理念基本能够用一句归纳:遵循可扩展开发,可快速迭代,可持续部署,可的架构设计,追求目标应用场景下最优于团队的技术选型决策web

因此语言之争也好,平台之争也好,落到每个对编程和解决问题感兴趣的开发者身上,便成了最微不足道的问题。可以感觉不一样技术间的碰撞,领略到不一样架构思想中的精妙,就已是一件知足的事情了,等到团队须要你快速应用其余技术选型时,以前的努力也是助力。固然面向工资编程也是一种取舍,笔者思考的时候也会陷入这个怪圈,因此但愿在不断的学习和实践中,可以让本身更满意吧。编程

著名的 DRY 原则告诉咱们 —— Don't repeat yourself,而笔者想更进一步的是,Deep Dive And Wide Mind,深刻更多和尝试更多。性能优化

奇怪的前言就此结束。服务器

做为最新的正式版本,虽然版本号只是小小的提高,可是 .NET Core 2.1 相比 .NET Core 2.0 在性能上又有了大大的提高。不管是项目构建速度,仍是字符串操做,网络传输和 JIT 内联方法性能,能够这么说的是,现在的 .NET Core 已经主动为开发者带来抠到字节上的节省体验。具体的介绍还请参看 Performance Improvements in .NET Core 2.1网络

而在这篇文章里,笔者要聊聊的只是关于 async/await 的一些底层原理和 .NET Core 2.1 在异步操做对象分配上的优化操做。架构

async/await 实现简介

熟悉异步操做的开发者都知道,async/await 的实现基本上来讲是一个骨架代码(Template method)和状态机。框架

从反编译器中咱们能够窥见骨架方法的全貌。假设有这样一个示例程序asp.net

internal class Program
{
    private static void Main()
    {
        var result = AsyncMethods.CallMethodAsync("async/await").GetAwaiter().GetResult();

        Console.WriteLine(result);
    }
}

internal static class AsyncMethods
{
    internal static async Task<int> CallMethodAsync(string arg)
    {
        var result = await MethodAsync(arg);

        await Task.Delay(result);

        return result;
    }

    private static async Task<int> MethodAsync(string arg)
    {
        var total = arg.First() + arg.Last();

        await Task.Delay(total);

        return total;
    }
}

为了能更好地显示编译代码,特意将异步操做分红两个方法来实现,即组成了一条异步操做链。这种“侵入性”传递对于开发实际上是更友好的,当代码中的一部分采用了异步代码,整个传递链条上便不得不采用异步这样一种正确的方式。接下来让咱们看看编译器针对上述异步方法生成的骨架方法和状态机(也已经通过美化产生可读的C#代码)。

[DebuggerStepThrough]
[AsyncStateMachine((typeof(CallMethodAsyncStateMachine)]
private static Task<int> CallMethodAsync(string arg)
{
    CallMethodAsyncStateMachine stateMachine = new CallMethodAsyncStateMachine {
        arg = arg,
        builder = AsyncTaskMethodBuilder<int>.Create(),
        state = -1
    };
    stateMachine.builder.Start<CallMethodAsyncStateMachine>(
    (ref stateMachine)=>
    {
        // 骨架方法启动第一次 MoveNext 方法
        stateMachine.MoveNext();
    });
    
    return stateMachine.builder.Task;
}

[DebuggerStepThrough]
[AsyncStateMachine((typeof(MethodAsyncStateMachine)]
private static Task<int> MethodAsync(string arg)
{
    MethodAsyncStateMachine stateMachine = new MethodAsyncStateMachine {
        arg = arg,
        builder = AsyncTaskMethodBuilder<int>.Create(),
        state = -1
    };
    
    // 恢复委托函数
    Action __moveNext = () => 
    {
        stateMachine.builder.Start<CallMethodAsyncStateMachine>(ref stateMachine);
    }
    
    __moveNext();
    
    return stateMachine.builder.Task;
}
  • MethodAsync/CallMethodAsync - 骨架方法
  • MethodAsyncStateMachine/CallMethodAsyncStateMachine - 每一个 async 标记的异步操做都会产生一个骨架方法和状态机对象
  • arg - 显然原始代码上有多少个参数,生成的代码中就会有多少个字段
  • __moveNext - 恢复委托函数,对应状态机中的 MoveNext 方法,该委托函数会在执行过程当中做为回调函数返回给对应Task的 Awaiter 从而使得 MoveNext 持续执行
  • builder - 该结构负责链接状态机和骨架方法
  • state - 始终从 -1 开始,方法执行时状态也是1,非负值表明一个后续操做的目标,结束时状态为 -2
  • Task - 表明当前异步操做完成后传播的任务,其内包含正确结果

能够看到,每一个由 async 关键字标记的异步操做都会产生相应的骨架方法,而状态机也会在骨架方法中建立并运行。如下是实际的状态机内部代码,让咱们用实际进行包含两步异步操做的 CallMethodAsyncStateMachine 作例子。

[CompilerGenerated]
private sealed class CallMethodAsyncStateMachine : IAsyncStateMachine
{
    public int state;
    public string arg;  // 表明变量
    
    public AsyncTaskMethodBuilder<int> builder;
    
    // 表明 result
    private int result; 
    
    // 表明 var result = await MethodAsync(arg);
    private Task<int> firstTaskToAwait;  
    
    // 表明 await Task.Delay(result);
    private Task secondTaskToAwait; 

    private void MoveNext()
    {
        try
        {
            switch (this.state) // 初始值为-1
            {
                case -1: 
                    // 执行 await MethodAsync(arg);
                    this.firstTaskToAwait = AsyncMethods.MethodAsync(this.arg);
                    
                    // 当 firstTaskToAwait 执行完毕
                    this.result = firstTaskToAwait.Result;
                    this.state = 0;
                    
                    // 调用 this.MoveNext();
                    this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
                case 0:
                    // 执行 Task.Delay(result)
                    this.secondTaskToAwait = Task.Delay(this.result);
                    
                    // 当 secondTaskToAwait 执行完毕
                    this.state = 1;
                    
                    // 调用 this.MoveNext();
                    this.builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
                case 1:
                    this.builder.SetResult(result);
                    return;
            }
        }
        catch (Exception exception)
        {
            this.state = -2;
            this.builder.SetException(exception);
            return;
        }
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
}

能够看到一个异步方法内含有几个异步方法,状态机便会存在几种分支判断状况。根据每一个分支的执行状况,再经过调用 MoveNext 方法确保全部的异步方法可以完整执行。更进一步,看似是 switch 和 case 组成的分支方法,实质上仍然是一条异步操做执行和传递的Chain。

上述的 CallMethodAsync 方法也能够转化成如下 Task.ContinueWith 形式:

internal static async Task<int> CallMethodAsync(string arg)
{
    var result = await (
                    await MethodAsync(arg).ContinueWith(async MethodAsyncTask =>
                        {
                            var methodAsyncTaskResult = await MethodAsyncTask;
                            Console.Write(methodAsyncTaskResult);
                            await Task.Delay(methodAsyncTaskResult);
                            return methodAsyncTaskResult;
                        }));

    return result;
}

能够这样理解的是,整体看来,编译器每次遇到 await,当前执行的方法都会将方法的剩余部分注册为回调函数(当前 await 任务完成后接下来要进行的工做,也可能包含 await 任务,仍然能够顺序嵌套),而后当即返回(return builder.Task)。 剩余的每一个任务将以某种方式完成其操做(可能被调度到当前线程上做为事件运行,或者由于使用了 I/O 线程执行,或者在单独线程上继续执行,这其实并不重要),只有在前一个 await 任务标记完成的状况下,才能继续进行下一个 await 任务。有关这方面的奇思妙想,请参阅《经过 Await 暂停和播放》

.NET Core 2.1 性能提高

上节关于编译器生成的内容并不能彻底涵盖 async/await 的全部实现概念,甚至只是其中的一小部分,好比笔者并无提到可等待模式(IAwaitable)和执行上下文(ExecutionContext)的内容,前者是 async/await 实现的指导原则,后者则是实际执行异步代码,返回给调用者结果和线程同步的操控者。包括生成的状态机代码中,当第一次执行发现任务并未完成时(!awaiter.isCompleted),任务将直接返回。

主要缘由即是这些内容讲起来怕是要花很大的篇幅,有兴趣的同窗推荐去看《深刻理解C#》和 ExecutionContext

异步代码可以显著提升服务器的响应和吞吐性能。可是经过上述讲解,想必你们已经认识到为了实现异步操做,编译器要自动生成大量的骨架方法和状态机代码,应用一般也要分配更多的相关操做对象,线程调度同步也是耗时耗力,这也意味着异步操做运行性能一般要比同步代码要差(这和第一句的性能提高并不矛盾,体重更大的人可能速度下降了,可是抗击打能力也更强了)。

可是框架开发者一直在为这方面的提高做者努力,最新的 .NET Core 2.1 版本中也提到了这点。本来的应用中,一个基于 async/await 操做的任务将分配如下四个对象:

  1. 返回给调用方的Task
    任务实际完成时,调用方能够知道任务的返回值等信息
  2. 装箱到堆上的状态机信息
    以前的代码中,咱们用了ref标识一开始时,状态机实际以结构的形式存储在栈上,可是不可避免的,状态机运行时,须要被装箱到堆上以保留一些运行状态
  3. 传递给Awaiter的委托
    即前文的_moveNext,当链中的一个 Task 完成时,该委托被传递到下一个 Awaiter 执行 MoveNext 方法。
  4. 存储某些上下文(如ExecutionContext)信息的状态机执行者(MoveNextRunner

Performance Improvements in .NET Core 2.1 一文介绍:

for (int i = 0; i < 1000; i++)
{
    await Yield();
    async Task Yield() => await Task.Yield();
}

当前的应用将分配下图中的对象:

此处输入图片的描述

而在 .NET Core 2.1 中,最终的分配对象将只有:

此处输入图片的描述

四个分配对象最终减小到一个,分配空间也缩减到了过去的一半。更多的实现信息能够参考 Avoid async method delegate allocation

结语

本文主要介绍了 async/await 的实现和 .NET Core 2.1 中关于异步操做性能优化的相关内容。由于笔者水平通常,文章篇幅有限,不能尽善尽美地解释完整,还但愿你们见谅。

不管是在什么平台上,异步操做都是重要的组成部分,而笔者以为任何开发者在会用之余,都应该更进一步地适当了解背后的故事。具体发展中,C# 借鉴了 F#中的异步实现,其余语言诸如 js 可能也借鉴了 C# 中的部份内容,固然一些基本术语,好比回调或是 feature,任何地方都是类似的,怎么都脱离不开计算机体系,这也说明了编程基础的重要性。

参考文献

  1. 经过 Await 暂停和播放
  2. 经过新的 Visual Studio Async CTP 更轻松地进行异步编程
相关文章
相关标签/搜索