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. 数学--数论--欧拉降幂--P5091 欧拉定理

    题目背景 出题人也想写有趣的题面,可惜并没有能力. 题目描述 给你三个正整数,a,m,ba,m,ba,m,b,你需要求:ab mod ma^b \bmod mabmodm 输入格式 一行三个整数,a, ...

  2. UDP广播的客户端和服务器端的代码设计

    实验环境 linux 注意: 使用UDP广播,是客户端发送广播消息,服务器端接收消息.实际上是客户端探测局域网中可用服务器的一种手段.客户端发送,服务器端接收,千万不能弄混淆!!! 为了避免混淆,本文 ...

  3. RF(三层封装设计)

    一.用例分层思想 元素层:需要导入 Selenium2Library 库 包含所有的元素定位 流程层:需要导入 元素层.txt 资源 封装用例流程 案例层:需要导入 流程层.txt 资源 输出用例,传 ...

  4. 精通awk系列文章

    精通awk系列文章 我录制了两个awk相关的视频教程: Awk经典实战案例精讲 精通awk精品课程:awk从入门到精通 1.安装新版本的gawk 2.本教程测试所用示例文件 3.铺垫知识:读取文件的几 ...

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

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

  6. 03_CSS入门和高级技巧(1)

    上节课知识的复习 插入图片,页面中能够插入的图片类型:jpg.jpeg.bmp.png.gif:不能的psd.fw. 语法: <img src="路径" alt=" ...

  7. 一文教你快速搞懂速度曲线规划之S形曲线(超详细+图文+推导+附件代码)

    本文介绍了运动控制终的S曲线,通过matlab和C语言实现并进行仿真:本文篇幅较长,请自备茶水: 请帮忙点个赞

  8. uCOS2014.1.7

    主要关于任务堆栈: 在计算机中一般设置一个专用的地址寄存器用来存放堆栈的栈顶地址,这个寄存器称为堆栈指针(SP). 任务堆栈有两种,一种是地址向下增长的,PC就是采用这样的堆栈: 另一种是地址向上增长 ...

  9. 使用 pyautogui 进行跨平台的 GUI 自动化操作

    有个朋友最近问我有没有推荐 GUI 桌面应用自动化的技术,我只能回答他:不好意思,这个真有,他是 pyautogui.主要有三大特征: 纯纯的 python, 源码一览无余: 跨平台,linux, w ...

  10. MYsql 8 连接报错 MySQLNonTransientConnectionException: Could not create connection to database server.

    本地安装mysql 是8 项目中数据驱动 也要求是 8 <dependency> <groupId>mysql</groupId> <artifactId&g ...