C# Under the Hood: async/await

原文地址:https://www.markopapic.com/csharp-under-the-hood-async-await/

前言

Async 和 await 关键字是在 C# 5 版本中提出的,作为一种很酷的特征用来处理异步任务。它们允许我们以十分简单、直觉的方式来指定将被异步执行的任务。然而,一些人仍然迷惑于异步编程,并且不确定它是如何工作的。我将向你展示当使用 async 和 await 时底下的魔法。

Awaiter Pattern

异步等待者模式

C# 语言编译它一些特征(关键字)称之为语法糖,语法糖的意思是这些语法仅只是现有语法的一种方便的表达。许多这样的语法糖被解释为模式。这些模式都基于方法调用,属性查看或接口实现。 await 表达式就是这些语法糖中的一个。它利用一个基于一些方法调用的模式。为了得到一个可等待的类型,它需要符合以下需求:

它必须具有以下方法:

INotifyCompletion GetAwaiter()

GetAwaiter 方法的返回类型需要实现 INotifyCompletion 接口,并且还要:

  • 有一个属性:bool IsCompleted
  • 有一个方法:void GetResult()

如果你去看 Task 类的源码,就会发现它符合上述所有需求。

所以,一个类型甚至并不需要实现某些特定的接口才可以成为可等待(类型),只需要有一个特定签名的方法。如图鸭子类(别):

如果一个动物像鸭子一样走路,并且嘎嘎的叫,那它就是一只鸭子。

当前的案例是:

如果一个类型具有特定签名的某个方法,那它就是可等待的。

为了给你一个说明的例子,我将创建一些自定义的类型,并使其可等待。所以,下面是我定义的类:

public class MyAwaitableClass

{

}

当我尝试等待 MyAwaitableClass 类型的实例,我获得了下面的错误:

它说:‘MyAwaitableClass’ 没有包含 ‘GetAwaiter’ 的定义,没有接受第一个参数是 ‘MyAwaitableClass’ 的 ‘GetAwaiter’ 扩展方法,你是否缺失一个 using 指令或程序集引用?

让我们向我们的类添加一个 GetAwaiter’  方法:

public class MyAwaitableClass

{

    public MyAwaiter GetAwaiter()

    {

        return new MyAwaiter();

    }

}

public class MyAwaiter

{

    public bool IsCompleted

    {

        get { return false; }

    }

}

我们可以看到编译器提示错误改不了:

现在它说:‘MyAwaiter’ 没有实现 ‘INotifyCompletion’

好吧,让我们在 MyAwaiter 类中实现 INotifyCompletion 接口:

public class MyAwaiter : INotifyCompletion

{

    public bool IsCompleted

    {

        get { return false; }

    }

    public void OnCompleted(Action continuation)

    {

    }

}

  

接下来,看到的编译器提示错误像这样:

它说:‘MyAwaiter’ 没有包含 ‘GetResult’ 的定义。

所以,我们添加一个 GetResult 方法,于是现在的代码如下:

public class MyAwaitableClass

{

    public MyAwaiter GetAwaiter()

    {

        return new MyAwaiter();

    }

}

public class MyAwaiter : INotifyCompletion

{

    public void GetResult()

    {

    }

    public bool IsCompleted

    {

        get { return false; }

    }

    //From INotifyCompletion

    public void OnCompleted(Action continuation)

    {

    }

}

我们将看到这儿编译器没有提示错误:

这意味着我们创建了一个可等待类型。

到此,我们明白了 await 表达式模式,那么我们来看一看 async 和 await 一起使用时到底发生了什么。

Async

异步

对于每一个 async 方法都产生了一个 state machine (状态机)。状态机是一种实现了 来自 System.Runtime.CompilerServices 命名空间IAsyncStateMachine 接口的结构(struct)。这个接口仅只被编译器使用,且有以下方法:

MoveNext() 切换到状态机的下一个状态

SetStateMachine(IAsyncStateMachine) 使用堆中的 IAsyncStateMachine 配置状态机。

现在,让我们来看一下下面的代码:

class Program

{

    static void Main(string[] args)

    {

    }

    static async Task FooAsync()

    {

        Console.WriteLine("Async method that doesn't have await");

    }

}

我们有一个异步的 FooAsync 方法。你可能注意到它缺少 await 操作,但是在当前出于简化的目的我省略了它。

现在我们来看一下编译器将这个方法编译后的代码。我使用 dotPeek 工具来反编译前面生成 dll 文件。为了看到幕布后面的内容,你需要启用 dotPeek 中的 Show Compiler-generated Code 选项。

编译器生成的类通常在类名中含有 < 和 > 符号,这在 C# 标识符中无效,所以这样就不好与用户创建的制品(类型)有冲突。

让我们看一看编译器为我们的 FooAsync 方法生成的内容:

我们的 Program 类包含期望的 Main 和 FooAsync 方法,但是我们也可以看到编译器生成了一个 Progr.<FooAsync>d__1 的结构(struct)。这个结构是一个实现了 IAsyncStateMachine 接口的状态机。除了 IAsyncStateMachine 接口具有的方法外,还具有以下字段:

<>1__state 表明状态机当前的状态

<>t__builder 的类型是 AsyncTaskMethodBuilder ,这个类型被用于创建异步方法和产生用来返回的任务,AsyncTaskMethodBuilder 结构也是出于让编译器使用的。

我们将看到这个结构更加详细的代码,不过在此之前我们先看一下通过反编译出的 FooAsync 方法:

private static Task FooAsync()

{

  Program.<FooAsync>d__1 stateMachine;

  stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();

  stateMachine.<>1__state = -;

  stateMachine.<>t__builder.Start<Program.<FooAsync>d__1>(ref stateMachine);

  return stateMachine.<>t__builder.Task;

}

这是编译器将异步方法转换后的样子。在方法内部的代码做了以下事情:

  • 实例化了这个方法的状态机
  • 创建了 AsyncTaskMethodBuilder 实例,并赋值给状态机的 builder 属性
  • 设置状态机为开始状态
  • 通过调用 Start 方法开始 builder,并传递当前的状态机
  • 返回任务

也许你会注意到,编译器产生的 FooAsync 方法没有包含任何我们原始 FooAsync 方法的内容。而原始方法的内容代表着这个方法的功能,那么它去哪儿呢?原始代码内容转移到状态机的 MoveNext 方法中。现在让我们来看一看 Program.<FooAsync>d_1 结构的内容:

[CompilerGenerated]

[StructLayout(LayoutKind.Auto)]

private struct <FooAsync>d__1 : IAsyncStateMachine

{

  public int <>1__state;

  public AsyncTaskMethodBuilder <>t__builder;

  void IAsyncStateMachine.MoveNext()

  {

       try

       {

         Console.WriteLine("Async method that doesn't have await");

       }

       catch (Exception ex)

       {

         this.<>1__state = -;

         this.<>t__builder.SetException(ex);

         return;

       }

       this.<>1__state = -;

       this.<>t__builder.SetResult();

  }

  [DebuggerHidden]

  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)

  {

       this.<>t__builder.SetStateMachine(stateMachine);

  }

}

MoveNext 方法在 try 块中包含原始方法内容。当一些异常在我们的代码中发生,异常将被给到当前方法中的 builder ,最终传递给任务(FooAsync 方法返回的任务)。在此之后,调用 builder 的 SetResult 方法指出任务已经完成。

现在,我们已经看到异步方法的内幕。出于简洁的目的,我没有在 FooAsync 方法中放置任何的 await 操作,所以我们的状态机没有更多的状态转换。它仅只是执行我们的方法内容并到达完成状态,也就是说,我们的方法是以同步的方法执行。现在是时候看一看,当方法中含有 await 操作时 MoveNext 方法长什么样子。

让我们看一下下面的方法:

static async Task BarAsync()

{

    Console.WriteLine("This happens before await");

    int i = await QuxAsync();

    Console.WriteLine("This happens after await. The result of await is " + i);

}

它等待 QuxAsync 方法并使用这个任务的结果。

如果我们使用 dotPeek 反编译它,我们就会发现编译器产生的代码和之前 FooAsync 方法(编译器产生的代码)具有相同的结果,即便现在的 FooAsync 方法有所不同:

private static Task BarAsync()

{

  Program.<BarAsync>d__2 stateMachine;

  stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();

  stateMachine.<>1__state = -;

  stateMachine.<>t__builder.Start<Program.<BarAsync>d__2>(ref stateMachine);

  return stateMachine.<>t__builder.Task;

}

而不同之处在于状态机的 MoveNext 方法,现在我们的方法包含 await 表达式,状态机的代码如下:

[CompilerGenerated]

[StructLayout(LayoutKind.Auto)]

private struct <BarAsync>d__2 : IAsyncStateMachine

{

  public int <>1__state;

  public AsyncTaskMethodBuilder <>t__builder;

  private TaskAwaiter<int> <>u__1;

  void IAsyncStateMachine.MoveNext()

  {

       int num1 = this.<>1__state;

       try

       {

         TaskAwaiter<int> awaiter;

         int num2;

         if (num1 != )

         {

              Console.WriteLine("This happens before await");

              awaiter = Program.QuxAsync().GetAwaiter();

              if (!awaiter.IsCompleted)

              {

                this.<>1__state = num2 = ;

                this.<>u__1 = awaiter;

                this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<BarAsync>d__2>(ref awaiter, ref this);

                return;

              }

         }

         else

         {

              awaiter = this.<>u__1;

              this.<>u__1 = new TaskAwaiter<int>();

              this.<>1__state = num2 = -;

         }

         Console.WriteLine("This happens after await. The result of await is " + (object) awaiter.GetResult());

       }

       catch (Exception ex)

       {

         this.<>1__state = -;

         this.<>t__builder.SetException(ex);

         return;

       }

       this.<>1__state = -;

       this.<>t__builder.SetResult();

  }

  [DebuggerHidden]

  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)

  {

       this.<>t__builder.SetStateMachine(stateMachine);

  }

}

下面的图片解释了上面状态机:

所以,await 到底做了什么如下所示:

总结

每一次你创建一个异步方法,编译器为其生成一个状态机,然后其中的每一个 await 操作,它做了如下的事情:

  • 执行当前方法直到 await 表达式
  • 检查当前可等待的任务是否完成
    1. 如果完成,执行方法中剩下的内容
    2. 如果没有完成,使用回调去执行方法中剩下的内容,而回调会在可等待任务完成时调用

参考

Task 源码:

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?redirectedfrom=MSDN&view=netframework-4.8#methods

原文地址:

https://www.markopapic.com/csharp-under-the-hood-async-await/

IAsyncStateMachine 接口

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.iasyncstatemachine?redirectedfrom=MSDN&view=netframework-4.8

dotPeek

https://www.jetbrains.com/decompiler/

start

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asynctaskmethodbuilder.start?redirectedfrom=MSDN&view=netframework-4.8#System_Runtime_CompilerServices_AsyncTaskMethodBuilder_Start__1___0__

SetResult

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asynctaskmethodbuilder.setresult?redirectedfrom=MSDN&view=netframework-4.8#System_Runtime_CompilerServices_AsyncTaskMethodBuilder_SetResult

async/await 内幕【译文】的更多相关文章

  1. 译文: async/await SynchronizationContext 上下文问题

    async / await 使异步代码更容易写,因为它隐藏了很多细节. 许多这些细节都捕获在 SynchronizationContext 中,这些可能会改变异步代码的行为完全由于你执行你的代码的环境 ...

  2. 译文:TransactionScope 与 Async/Await

    你可能不知道这一点,在 .NET Framework 4.5.0  版本中包含有一个关于 System.Transactions.TransactionScope 在与 async/await 一起工 ...

  3. (译文)学习ES6非常棒的特性——Async / Await函数

    try/catch 在使用Async/Await前,我们可能这样写: const main = (paramsA, paramsB, paramsC, done) => { funcA(para ...

  4. [译]async/await中阻塞死锁

    这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...

  5. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  6. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  7. JavaScript 如何工作的: 事件循环和异步编程的崛起 + 5 个关于如何使用 async/await 编写更好的技巧

    原文地址:How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding wi ...

  8. 图与例解读Async/Await

    JavaScript ES7的async/await语法让异步promise操作起来更方便.如果你需要从多个数据库或者接口按顺序异步获取数据,你可能最终写出一坨纠缠不清的promise与回调.然而使用 ...

  9. 【译】Async/Await(三)——Aysnc/Await模式

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

随机推荐

  1. Mysql 字符串拆分 OR 一行转多行

    Mysql 字符串拆分 OR 一行转多行 需要了解的的几个mysql 函数: A.substring_index():字符串截取 substring_index(str,delim,count)   ...

  2. JAVA大数--POJ 1715 大菲波数

    Problem Description Fibonacci数列,定义如下: f(1)=f(2)=1 f(n)=f(n-1)+f(n-2) n>=3. 计算第n项Fibonacci数值.  Inp ...

  3. POJ - 2387 Til the Cows Come Home (最短路入门)

    Bessie is out in the field and wants to get back to the barn to get as much sleep as possible before ...

  4. centos下配置LNMP环境(源码安装)

    准备工作,安装依赖库 yum -y install gcc automake autoconf libtool make gcc-c++ glibc libxslt-devel libjpeg lib ...

  5. A. Powered Addition(二进制性质-思维)

    \(拿样例来看1 7 6 5\) \(6成长到7是最合理的,因为1s就可以实现而且对于后面来说最优\) \(5成长到7是最合理的,因为2s就可以实现而且对于后面最优\) \(发现了什么?二进制是可以组 ...

  6. C. Journey bfs 拓扑排序+dp

    C. Journey 补今天早训 这个是一个dp,开始我以为是一个图论,然后就写了一个dij和网络流,然后mle了,不过我觉得如果空间开的足够的,应该也是可以过的. 然后看了题解说是一个dp,这个dp ...

  7. NDK clang编译器的一个bug

    NDK clang编译器的一个bug 问题代码 float32_t Sum_float(float32_t *data, const int count) { float32x4_t res = vd ...

  8. # C#学习笔记(一)——准备工作

    C#学习笔记(一)--准备工作 目录 C#学习笔记(一)--准备工作 1.1 下载安装.NET框架 1.2 创建源代码 1.3 一些基本名称 1.4 简单的命名建议 1.1 下载安装.NET框架 .N ...

  9. 【Hadoop离线基础总结】Hadoop High Availability\Hadoop基础环境增强

    目录 简单介绍 Hadoop HA 概述 集群搭建规划 集群搭建 第一步:停止服务 第二步:启动所有节点的ZooKeeper 第三步:更改配置文件 第四步:启动服务 简单介绍 Hadoop HA 概述 ...

  10. STM32 Cube之旅-尝试新的开发方式

    尝试使用Cube进行一些开发学习,这里对此做一个梗概,先有一个全面的了解. 文章目录 Cube全家桶 CubeMX CubeIDE CubeProg 结语 Cube全家桶 曾几何时,ST刚推出Cube ...