async/await

电脑编程中,async/await模式是一种存在于许多编程语言中的语法特性。这种模式使得异步非阻塞函数逻辑可以用一种类似于同步函数的方式进行构造。在语义上,它与协程的概念相关,且通常也使用类似的技术实现。该模式大都是为了让程序能够在等待一个异步的长耗时操作完成的同时,也可以正常执行代码,它通常表现为Promises或者类似的形式。

这一特性出现在C# 5.0[1]:10C++20Python 3.5、 F#HackJuliaDartKotlin 1.1、 Rust 1.39、[2] Nim 0.9.4、[3] JavaScript ES2017Swift 5.5[4]Zig[5]中。对于Scala则出现在一些beta版本、实验版本的插件和特定的一些实现中。[6]

历史

F# 2.0(2007)中添加了含await点的异步逻辑[7]这对于后来添加到C#中的async/await机制有所启发。[8]

微软在2011年的Async CTP(Community Technology Preview,社区技术预览版)C#中首次添加了async/await,随后在2012年的C# 5中正式发布了这一机制。[9][1]:10

Haskell的主要开发者Simon Marlow英语Simon Marlow于2012年开发了async包。[10]

Python 3.5(2015)中支持了async/await,加入两个新的关键字asyncawait[11]

TypeScript 1.7(2015)中支持了async/await。[12]

JavaScript在2017年支持了async/await,作为ES2017标准的一部分。

Rust 1.39.0(2019)中支持了async/await,加入一个关键字async和一个惰性求值await[13][14]

C++20(2020)中支持了async/await,加入三个新的关键字co_returnco_awaitco_yield

Swift 5.5(2021)中支持了async/await,加入三个新的关键字asyncawaitactor,其中actor来自于同时期发布的对演员模型的一种具体的实现。这一模型中直接用到了async/await

一个C#的例子

下面的函数用于从一个URI上下载数据,然后返回这个数据的长度,其中就是用了async/await模式。

public async Task<int> FindPageSizeAsync(Uri uri) 
{
    var client = new HttpClient();
    byte[] data = await client.GetByteArrayAsync(uri);
    return data.Length;
}

为了便于理解,我们将上面的这一函数称为主函数。注意下文中的Promise是一个概念上统称,而不一定是具体存在的一种对象。

  1. 当函数被async关键字标记时,它会被编译器认为是异步的。这样的函数中可能会有若干个await表达式,它们可将结果绑定到Promise对象上。[15]:165-168主函数就是一个典型的异步函数。
  2. 主函数的返回类型 Task<T>(其中T为泛型)是C#对Promise概念的一种实现,在代码中Task<int>表明该Promise对应的实际结果是int类型的。
  3. 当主函数被执行时,首先一个新的HttpClient实例会被赋给client。
  4. 接下来await后紧跟的一句表达式执行了client上的异步方法GetByteArrayAsync(uri)[16]:189-190, 344[1]:882,它会返回一个Task<byte[]>。由于该方法是异步的,它并不会等到下载完成才返回,而是以某种非阻塞的方式(例如后台进程)开始下载,然后马上返回一个没有被resolve也没有被rejectTask<byte[]>到主函数中。在这里,resolve和reject可以理解为Task对象上的两个自带的方法,resolve(x)表示执行完成,reject(x)表示执行失败,二者都表示执行结束,因而都可用于最终的传值。
  5. 由于该表达式返回的Task<byte[]>前有await,接下来主函数会直接返回一个类似于之前Task对象的Task<int>到其调用者。显然,其调用者并不会被阻塞。
  6. GetByteArrayAsync(uri)下载结束后,它会使用其下载的数据resolve它所返回的那个Task,即上文中的Task<byte>Resolve会触发一个回调函数,使得主函数继续向下执行return data.Length
  7. 然后与GetByteArrayAsync(uri)的行为类似,主函数也会使用return语句返回的值来resolve它所返回的Task<int>,触发一个回调函数,使得其调用者能够开始使用这一具体值。

异步函数内部可以根据需要使用多个await语句,每一个语句都会以相同的方式进行处理(实际上只有第一个await语句会返回Promise,其余的await都用机制类似的内部回调函数实现)。对于返回的Promise对象,算法中亦可以对其直接进行处理(例如先保存起来),从而实现先执行其它任务(包括触发新的异步Task),等到需要相关结果的时候才使用await语句处理Promise对象,拿到结果。

除了直接await之外,也有一些可以批量处理Promise对象的函数,比如C#中的Task.WhenAll()函数[1]:174-175[16]:664-665,它会返回一个Task(无值Task,可以理解为Task<void>)。这个被返回的Task会在Task.WhenAll()方法参数中提供的所有的Promise被resolve以后resolve。还有一些Promise返回类型支持通常async/await模式不会用到的一些方法,例如对Promise设置多个结果的回调函数、监听长耗时Task的执行进程等。

在C#和许多其它语言中,async/await模式并不是运行时的核心组成部分,而实际上会在编译的时候使用Lambda表达式或者续体来实现。例如上面的C#代码很可能会被编译器先转换成下面的代码,然后才被转换成字节码

public Task<int> FindPageSizeAsync(Uri uri) 
{
    var client = new HttpClient();
    Task<byte[]> dataTask = client.GetByteArrayAsync(uri);
    Task<int> afterDataTask = dataTask.ContinueWith((originalTask) => {
        return originalTask.Result.Length;
    });
    return afterDataTask;
}

也正因此,如果某个函数需要返回一个Promise对象,但是其本身并不需要进行任何的await求值,那么它并不需要在函数声明前面加上async来让自己成为一个异步函数,而是可以直接返回一个Promise对象。例如,使用C#的Task.FromResult()方法[16]:656来返回一个马上resolve的Task,或者直接如同地铁换乘一样将其它函数所提供的Task直接原样返回。 不过对于这项功能必须要注意的一项是,尽管在异步函数内部的逻辑长得很像同步的形式,这些逻辑实际上是非阻塞的,甚至可能是多线程的。所以在await语句等待Promise被resolve的时候,可能会发生许多侵入性的事件。比如下面的代码,如果没有await,那么将始终执行成功,但是如果使用了async/await模式,就可能会出现state.a发生改变(被其它逻辑)的情况。

var a = state.a;
var client = new HttpClient(); // 与a无关的语句
var data = await client.GetByteArrayAsync(uri); // 与a无关的语句
Debug.Assert(a == state.a); // ★这个语句可能会出现错误,因为state.a可能在await的过程中被其它逻辑篡改。
return data.Length;

在F#中的使用

F#中的异步逻辑的具体实现是计算表达式。具体使用时并不需要加上特殊的标识符,例如async。在代码逻辑中,使用一个感叹号(!)来开始异步操作。

从URL下载数据的异步函数逻辑如下:

let asyncSumPageSizes (uris: #seq<Uri>) : Async<int> = async {
    use httpClient = new HttpClient()
    let! pages = 
        uris
        |> Seq.map(httpClient.GetStringAsync >> Async.AwaitTask)
        |> Async.Parallel
    return pages |> Seq.fold (fun accumulator current -> current.Length + accumulator) 0
}

在C#中的使用

微软将C#中的async/await模式称作“以任务为基础的异步模式”(Task-based Asynchronous Pattern,TAP)。[17]异步函数的返回值通常包括voidTaskTask<T>[16]:35[18]:546-547[1]:22, 182 ValueTaskValueTask<T>[16]:651-652[1]:182-184代码中还可以利用异步函数构造器(async method builders)自行定义异步函数的返回值类型,不过这一场景高阶且少见。[19]返回void的异步函数通常应该是事件监听器,对于一般的函数则应该返回Task对象,因为它能够提供更加直观的异常处理[20]

要在函数中使用await,必须在函数声明前加上async关键字。当需要函数返回Task<T>类型的值时,函数声明前要加上async关键字,同时应当返回T或兼容的类型,而非Task<T>本身;随后编译器就会将返回的T类型包装为Task<T>泛型。不过,当非异步函数(没有使用async声明的函数)返回Task<T>时,其值也可以被await

下列函数代码将使用await从URL下载数据。该函数的逻辑用await实现了同时触发多个任务,无需等待其完成,这使得下一个任务无需在上一个任务完成之后才触发(这是同步的逻辑)。

public async Task<int> SumPageSizesAsync(IEnumerable<Uri> uris) 
{
    var client = new HttpClient();
    int total = 0;
    var loadUriTasks = new List<Task<byte[]>>();

    foreach (var uri in uris)
    {
        var loadUriTask = client.GetByteArrayAsync(uri);
        loadUriTasks.Add(loadUriTask );
    }

    foreach (var loadUriTask in loadUriTasks)
    {
        statusText.Text = $"已找到 {total} 个字节...";
        var resourceAsBytes = await loadUriTask;
        total += resourceAsBytes.Length;
    }

    statusText.Text = $"共找到 {total} 个字节。";

    return total;
}

在Scala中的使用

Scala中有一个实验性的拓展Scala-async可以实现async/await模式。它提供了一个名为await的特殊函数。[6]与C#不同的是,Scala中的异步逻辑并不需要用async来标记。通过Scala-async,可以直接将异步逻辑用async函数调用的形式包围起来。

这是如何实现的

Scala-async所提供的async实际上是通过来实现的。编译器会调用不同的代码,然后产生一个有限状态机(通常认为这比单子实现更高效,但是更难以编写)。

在Python中的使用

在Python中的使用,在语法上与C#、JavaScript等类似。

import asyncio

async def main():
    print("hello")
    await asyncio.sleep(1)
    print("world")

asyncio.run(main())

在JavaScript中的使用

JavaScript中的await运算符只能用于async标注的函数中,或者用于模块的最顶层。

如果await运算符后跟参数为Promise对象,那么函数逻辑会在该Promise对象被resolve之后继续执行,或者在它被reject以后抛出异常(可以进行异常处理);如果await运算符后跟参数不是Promise对象,那么该值会被直接返回(不会等待)。[21]

许多JavaScript库提供了可返回Promise对象的函数,它们都可以被await——只要符合JavaScript中的Promise规范。JQuery中函数返回的Promise在3.0版本以后才达到了Promises/A+兼容度。[22]

下面是一个使用例[23]

async function createNewDoc() {
  let response = await db.post({}); // post a new doc
  return db.get(response.id); // find by id
}

async function main() {
  try {
    let doc = await createNewDoc();
    console.log(doc);
  } catch (err) {
    console.log(err);
  }
}
main();

Node.js 8 中包含的一样工具可将标准库中利用回调模式编写的函数当作Promise来使用。[24]

在C++中的使用

C++ 20中正式支持了await(在C++中是co_await)。GCCMSVC编译器支持async/await模式,包括协程以及相关的关键字例如co_awaitClang对此有部分支持。

值得注意的是std::promisestd::future虽然看起来像是可以被await求值的对象,但是实际上它们并没有实现任何类似于从协程中返回的值或者可被await的对象的相关属性;要使返回对象可以被await求值,必须在返回的对象类型上实现一系列的公共成员函数,例如await_readyawait_suspendawait_resume等。具体细节可以查看相关的参考。[25]

#include <iostream>
#include "CustomAwaitableTask.h"

using namespace std;

CustomAwaitableTask<int> add(int a, int b)
{
    int c = a + b;
    co_return c;
}

CustomAwaitableTask<int> test()
{
    int ret = co_await add(1, 2);
    cout << "return " << ret << endl;
    co_return ret;
}

int main()
{
    auto task = test();

    return 0;
}

在C语言中的使用

C语言没有对await/async的官方支持。

某些协程库(例如s_task页面存档备份,存于互联网档案馆))通过宏定义的方式, 实现了和其他语言类似的await/async的强制性语义要求,即:

1. 必须在async标注的函数内,才能调用await;
2. 等待一个标注为aysnc的函数,调用该函数时需要加上await;
#include <stdio.h>
#include "s_task.h"

//定义协程任务需要的栈空间
int g_stack_main[64 * 1024 / sizeof(int)];
int g_stack0[64 * 1024 / sizeof(int)];
int g_stack1[64 * 1024 / sizeof(int)];

void sub_task(__async__, void* arg) {
    int i;
    int n = (int)(size_t)arg;
    for (i = 0; i < 5; ++i) {
        printf("task %d, delay seconds = %d, i = %d\n", n, n, i);
        s_task_msleep(__await__, n * 1000);  //等待一点时间
    }
}

void main_task(__async__, void* arg) {
    int i;

    //创建两个子任务
    s_task_create(g_stack0, sizeof(g_stack0), sub_task, (void*)1);
    s_task_create(g_stack1, sizeof(g_stack1), sub_task, (void*)2);

    for (i = 0; i < 4; ++i) {
        printf("task_main arg = %p, i = %d\n", arg, i);
        s_task_yield(__await__); //主动让出cpu
    }

    //等待子任务结束
    s_task_join(__await__, g_stack0);
    s_task_join(__await__, g_stack1);
}

int main(int argc, char* argv) {

    s_task_init_system();

    //创建一个任务
    s_task_create(g_stack_main, sizeof(g_stack_main), main_task, (void*)(size_t)argc);
    s_task_join(__await__, g_stack_main);
    printf("all task is over\n");
    return 0;
}

参考文献

  1. ^ 1.0 1.1 1.2 1.3 1.4 1.5 Skeet, Jon. C# in Depth. Manning. ISBN 978-1617294532. 
  2. ^ Announcing Rust 1.39.0. [2019-11-07]. (原始内容存档于2023-09-02) (英语). 
  3. ^ Version 0.9.4 released - Nim blog. [2020-01-19]. (原始内容存档于2023-08-01) (英语). 
  4. ^ Concurrency — The Swift Programming Language (Swift 5.5). docs.swift.org. [2021-09-28]. (原始内容存档于2022-03-01). 
  5. ^ Zig Language Reference. [2023-06-23]. (原始内容存档于2022-03-31). 
  6. ^ 6.0 6.1 Scala Async. GitHub. [20 October 2013]. (原始内容存档于2017-03-03). 
  7. ^ Syme, Don; Petricek, Tomas; Lomov, Dmitry. The F# Asynchronous Programming Model. Springer Link. Lecture Notes in Computer Science 6539. 2011: 175–189 [2021-04-29]. ISBN 978-3-642-18377-5. doi:10.1007/978-3-642-18378-2_15. (原始内容存档于2023-06-23) (英语). 
  8. ^ The Early History of F#, HOPL IV. ACM Digital Library. [2021-04-29]. (原始内容存档于2023-06-23) (英语). 
  9. ^ Hejlsberg, Anders. Anders Hejlsberg: Introducing Async – Simplifying Asynchronous Programming. Channel 9 MSDN. Microsoft. [5 January 2021]. (原始内容存档于2021-05-16) (英语). 
  10. ^ async: Run IO operations asynchronously and wait for their results. Hackage. [2023-06-23]. (原始内容存档于2023-06-23). 
  11. ^ What's New In Python 3.5 — Python 3.9.1 documentation. docs.python.org. [5 January 2021]. (原始内容存档于2016-06-18). 
  12. ^ Gaurav, Seth. Announcing TypeScript 1.7. TypeScript. Microsoft. 30 November 2015 [5 January 2021]. (原始内容存档于2023-07-24). 
  13. ^ Matsakis, Niko. Async-await on stable Rust! | Rust Blog. blog.rust-lang.org. Rust Blog. [5 January 2021]. (原始内容存档于2020-06-03) (英语). 
  14. ^ Rust Gets Zero-Cost Async/Await Support in Rust 1.39. [2023-06-23]. (原始内容存档于2023-06-23). 
  15. ^ Skeet, Jon. C# in Depth. Manning. ISBN 978-1617294532. 
  16. ^ 16.0 16.1 16.2 16.3 16.4 Albahari, Joseph. C# 10 in a Nutshell. O'Reilly. ISBN 978-1-098-12195-2. 
  17. ^ Task-based asynchronous pattern. Microsoft. [28 September 2020]. (原始内容存档于2022-09-03). 
  18. ^ Price, Mark J. C# 8.0 and .NET Core 3.0 – Modern Cross-Platform Development: Build Applications with C#, .NET Core, Entity Framework Core, ASP.NET Core, and ML.NET Using Visual Studio Code. Packt. ISBN 978-1-098-12195-2. 
  19. ^ Tepliakov, Sergey. Extending the async methods in C#. Developer Support. 2018-01-11 [2022-10-30]. (原始内容存档于2023-06-04) (美国英语). 
  20. ^ Stephen Cleary, Async/Await - Best Practices in Asynchronous Programming页面存档备份,存于互联网档案馆
  21. ^ await - JavaScript (MDN). [2 May 2017]. (原始内容存档于2017-06-02). 
  22. ^ jQuery Core 3.0 Upgrade Guide. [2 May 2017]. (原始内容存档于2021-01-21). 
  23. ^ Taming the asynchronous beast with ES7. [12 November 2015]. (原始内容存档于2015-11-15). 
  24. ^ Foundation, Node.js. Node v8.0.0 (Current) - Node.js. Node.js. [2023-06-27]. (原始内容存档于2023-10-03). 
  25. ^ Coroutines (C++20). [2023-06-27]. (原始内容存档于2021-03-25).