Async/await

zh:-{zh-hant|async/await}-;zh-cn:-{zh-cn|async/await}-;zh-hk:-{zh-hk|async/await}-;zh-mo:-{zh-mo|async/await}-;zh-my:-{zh-my|async/await}-;zh-sg:-{zh-sg|async/await}-;zh-tw:-{zh-tw|async/await}-#重定向在計算機程式設計中，async/await模式是一種存在於許多程式語言中的語法特性。這種模式使得非同步非阻塞函式邏輯可以用一種類似於同步函式的方式進行構造。在語義上，它與協程的概念相關，且通常也使用類似的技術實現。該模式大都是為了讓程式能夠在等待一個非同步的長耗時操作完成的同時，也可以正常執行程式碼，它通常表現為Promises或者類似的形式。這一特性出現在C# 5.0:10、C++20、 Python 3.5、 F#、 Hack、 Julia、 Dart、 Kotlin 1.1、 Rust 1.39、 Nim 0.9.4、 JavaScript ES2017、 Swift 5.5 和Zig中。對於Scala則出現在一些beta版本、實驗版本的外掛和特定的一些實現中。歷史.F# 2.0中添加了含await點的非同步邏輯。這對於後來新增到C#中的async/await機制有所啟發。微軟在2011年的Async CTPC#中首次添加了async/await，隨後在2012年的C# 5中正式釋出了這一機制。:10Haskell的主要開發者於2012年開發了async包。Python 3.5中支援了async/await，加入兩個新的關鍵字codice_1和codice_2。TypeScript 1.7中支援了async/await。JavaScript在2017年支援了async/await，作為ES2017標準的一部分。Rust 1.39.0中支援了async/await，加入一個關鍵字codice_1和一個惰性求值codice_2。C++20中支援了async/await，加入三個新的關鍵字codice_5、codice_6和codice_7。Swift 5.5中支援了async/await，加入三個新的關鍵字codice_1、codice_2和codice_10，其中codice_10來自於同時期釋出的對演員模型的一種具體的實現。這一模型中直接用到了async/await一個C#的例子.下面的函式用於從一個URI上下載資料，然後返回這個資料的長度，其中就是用了async/await模式。public async Task FindPageSizeAsync(Uri uri)var client = new HttpClient();byte[] data = await client.GetByteArrayAsync(uri);return data.Length;為了便於理解，我們將上面的這一函式稱為主函式。注意下文中的Promise是一個概念上統稱，而不一定是具體存在的一種物件。非同步函式內部可以根據需要使用多個await語句，每一個語句都會以相同的方式進行處理，等到需要相關結果的時候才使用await語句處理Promise物件，拿到結果。除了直接await之外，也有一些可以批次處理Promise物件的函式，比如C#中的codice_26函式:174-175:664-665，它會返回一個codice_27。這個被返回的codice_27會在codice_26方法引數中提供的所有的Promise被resolve以後resolve。還有一些Promise返回型別支援通常async/await模式不會用到的一些方法，例如對Promise設定多個結果的回撥函式、監聽長耗時Task的執行程序等。在C#和許多其它語言中，async/await模式並不是執行時的核心組成部分，而實際上會在編譯的時候使用Lambda表示式或者計算續體來實現。例如上面的C#程式碼很可能會被編譯器先轉換成下面的程式碼，然後才被轉換成位元組碼。public Task FindPageSizeAsync(Uri uri)var client = new HttpClient();Task dataTask = client.GetByteArrayAsync(uri);Task {return originalTask.Result.Length;return afterDataTask;也正因此，如果某個函式需要返回一個Promise物件，但是其本身並不需要進行任何的await求值，那麼它並不需要在函式宣告前面加上async來讓自己成為一個非同步函式，而是可以直接返回一個Promise物件。例如，使用C#的codice_31方法:656來返回一個馬上resolve的codice_27，或者直接如同地鐵換乘一樣將其它函式所提供的codice_27直接原樣返回。不過對於這項功能必須要注意的一項是，儘管在非同步函式內部的邏輯長得很像同步的形式，這些邏輯實際上是非阻塞的，甚至可能是多執行緒的。所以在await語句等待Promise被resolve的時候，可能會發生許多侵入性的事件。比如下面的程式碼，如果沒有await，那麼將始終執行成功，但是如果使用了async/await模式，就可能會出現codice_34發生改變的情況。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#中的非同步邏輯的具體實現是計算表示式。具體使用時並不需要加上特殊的識別符號，例如codice_1。在程式碼邏輯中，使用一個感嘆號來開始非同步操作。從URL下載資料的非同步函式邏輯如下：let asyncSumPageSizes (uris: #seq = async {use httpClient = new HttpClient()let! pages =uris|&gt; Seq.map(httpClient.GetStringAsync » Async.AwaitTask)|&gt; Async.Parallelreturn pages |&gt; Seq.fold (fun accumulator current -&gt; current.Length + accumulator) 0在C#中的使用.微軟將C#中的async/await模式稱作“以任務為基礎的非同步模式”自行定義非同步函式的返回值型別，不過這一場景高階且少見。返回codice_37的非同步函式通常應該是事件監聽器，對於一般的函式則應該返回codice_27物件，因為它能夠提供更加直觀的異常處理。要在函式中使用codice_2，必須在函式宣告前加上codice_1關鍵字。當需要函式返回codice_12型別的值時，函式宣告前要加上codice_1關鍵字，同時應當返回codice_48或相容的型別，而非codice_12本身；隨後編譯器就會將返回的codice_48型別包裝為codice_12泛型。不過，當非非同步函式返回codice_12時，其值也可以被codice_2。下列函式程式碼將使用codice_2從URL下載資料。該函式的邏輯用codice_2實現了同時觸發多個任務，無需等待其完成，這使得下一個任務無需在上一個任務完成之後才觸發。public async Task uris)var client = new HttpClient();int total = 0;var loadUriTasks = new List&lt;Task&lt;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模式。它提供了一個名為codice_2的特殊函式。與C#不同的是，Scala中的非同步邏輯並不需要用codice_1來標記。透過Scala-async，可以直接將非同步邏輯用codice_1函式呼叫的形式包圍起來。這是如何實現的.Scala-async所提供的codice_1實際上是透過宏來實現的。編譯器會呼叫不同的程式碼，然後產生一個有限狀態機。在Python中的使用.在Python中的使用，在語法上與C#、JavaScript等類似。import asyncioasync def main():print("hello")await asyncio.sleep(1)print("world")asyncio.run(main())在JavaScript中的使用.JavaScript中的codice_2運算子只能用於codice_1標註的函式中，或者用於模組的最頂層。如果codice_2運算子後跟引數為Promise物件，那麼函式邏輯會在該Promise物件被resolve之後繼續執行，或者在它被reject以後丟擲異常。許多JavaScript庫提供了可返回Promise物件的函式，它們都可以被codice_2——只要符合JavaScript中的Promise規範。JQuery中函式返回的Promise在3.0版本以後才達到了Promises/A+相容度。下面是一個使用例：async function createNewDoc() {let response = await db.post({}); // post a new docreturn db.get(response.id); // find by idasync function main() {try {let doc = await createNewDoc();console.log(doc);} catch (err) {console.log(err);main();Node.js 8 中包含的一樣工具可將標準庫中利用回撥模式編寫的函式當作Promise來使用。在C++中的使用.C++ 20中正式支援了await。GCC、MSVC編譯器支援async/await模式，包括協程以及相關的關鍵字例如codice_6。Clang對此有部分支援。值得注意的是codice_68和codice_69雖然看起來像是可以被await求值的物件，但是實際上它們並沒有實現任何類似於"從協程中返回的值"或者"可被await的物件"的相關屬性；要使返回物件可以被await求值，必須在返回的物件型別上實現一系列的公共成員函式，例如codice_70、codice_71和codice_72等。具體細節可以檢視相關的參考。using namespace std;CustomAwaitableTask add(int a, int b)int c = a + b;co_return c;CustomAwaitableTask 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的官方支援。某些協程庫透過宏定義的方式，實現了和其他語言類似的await/async的強制性語義要求，即：1. 必須在async標註的函式內，才能呼叫await；2. 等待一個標註為aysnc的函式，呼叫該函式時需要加上await；//定義協程任務需要的棧空間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 &lt; 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 &lt; 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;