在計算機編程中,async/await模式是一種存在於許多編程語言中的語法特性。這種模式使得異步非阻塞函數邏輯可以用一種類似於同步函數的方式進行構造。在語義上,它與協程的概念相關,且通常也使用類似的技術實現。該模式大都是為了讓程序能夠在等待一個異步的長耗時操作完成的同時,也可以正常執行代碼,它通常表現為Promises或者類似的形式。
這一特性出現在C# 5.0[1]:10、C++20、 Python 3.5、 F#、 Hack、 Julia、 Dart、 Kotlin 1.1、 Rust 1.39、[2] Nim 0.9.4、[3] JavaScript ES2017、 Swift 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於2012年開發了async包。[10]
Python 3.5(2015)中支持了async/await,加入兩個新的關鍵字async
和await
。[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_return
、co_await
和co_yield
。
Swift 5.5(2021)中支持了async/await,加入三個新的關鍵字async
、await
和actor
,其中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是一個概念上統稱,而不一定是具體存在的一種對象。
- 當函數被async關鍵字標記時,它會被編譯器認為是異步的。這樣的函數中可能會有若干個await表達式,它們可將結果綁定到Promise對象上。[15]:165-168主函數就是一個典型的異步函數。
- 主函數的返回類型
Task<T>
(其中T為泛型)是C#對Promise概念的一種實現,在代碼中Task<int>
表明該Promise對應的實際結果是int
類型的。 - 當主函數被執行時,首先一個新的HttpClient實例會被賦給client。
- 接下來await後緊跟的一句表達式執行了client上的異步方法
GetByteArrayAsync(uri)
[16]:189-190, 344[1]:882,它會返回一個Task<byte[]>
。由於該方法是異步的,它並不會等到下載完成才返回,而是以某種非阻塞的方式(例如後台進程)開始下載,然後馬上返回一個沒有被resolve也沒有被reject的Task<byte[]>
到主函數中。在這裡,resolve和reject可以理解為Task對象上的兩個自帶的方法,resolve(x)表示執行完成,reject(x)表示執行失敗,二者都表示執行結束,因而都可用於最終的傳值。 - 由於該表達式返回的
Task<byte[]>
前有await,接下來主函數會直接返回一個類似於之前Task對象的Task<int>
到其調用者。顯然,其調用者並不會被阻塞。 - 當
GetByteArrayAsync(uri)
下載結束後,它會使用其下載的數據resolve它所返回的那個Task,即上文中的Task<byte>
。Resolve會觸發一個回調函數,使得主函數繼續向下執行return data.Length
。 - 然後與
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]異步函數的返回值通常包括void
、Task
、Task<T>
、[16]:35[18]:546-547[1]:22, 182 ValueTask
和ValueTask<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();
在C++中的使用
C++ 20中正式支持了await(在C++中是co_await
)。GCC、MSVC編譯器支持async/await模式,包括協程以及相關的關鍵字例如co_await
。Clang對此有部分支持。
值得注意的是std::promise
和std::future
雖然看起來像是可以被await求值的對象,但是實際上它們並沒有實現任何類似於從協程中返回的值或者可被await的對象的相關屬性;要使返回對象可以被await求值,必須在返回的對象類型上實現一系列的公共成員函數,例如await_ready
、await_suspend
和await_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;
}
參考文獻
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.