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).