在計算機程式設計中，asyncawait 模式是一種存在於許多程式語言中的語法特性。這種模式使得非同步非阻塞函式邏輯可以用一種類似於同步函式的方式進行構造。在語義上，它與協程的概念相關，且通常也使用類似的技術實現。該模式大都是為了讓程式能夠在等待一個非同步的長耗時操作完成的同時，也可以正常執行程式碼，它通常表現為 Promises 或者類似的形式。 這一特性出現在 C 50、C20、 Python 35、 F、 Hack、 Julia、 Dart、 Kotlin 11、 Rust 139、 Nim 094、 JavaScript ES2017、 Swift 55 和 Zig 中。對於 Scala 則出現在一些 beta 版本、實驗版本的外掛和特定的一些實現中。 歷史 F 20（2007）中添加了含 await 點的非同步邏輯。這對於後來新增到 C 中的 asyncawait 機制有所啟發。 微軟在 2011 年的 Async CTP（Community Technology Preview，社群技術預覽版）C 中首次添加了 asyncawait，隨後在 2012 年的 C 5 中正式釋出了這一機制。 Haskell 的主要開發者於 2012 年開發了 async 包。 Python 35（2015）中支援了 asyncawait，加入兩個新的關鍵字 async 和 await。 TypeScript 17（2015）中支援了 asyncawait。 JavaScript 在 2017 年支援了 asyncawait，作為 ES2017 標準的一部分。 Rust 1390（2019）中支援了 asyncawait，加入一個關鍵字 async 和一個惰性求值 await。 C20（2020）中支援了 asyncawait，加入三個新的關鍵字 co_return、co_await 和 co_yield。 Swift 55（2021）中支援了 asyncawait，加入三個新的關鍵字 async、await 和 actor，其中 actor 來自於同時期釋出的對演員模型的一種具體的實現。這一模型中直接用到了 asyncawait 一個 C 的例子 下面的函式用於從一個 URI 上下載資料，然後返回這個資料的長度，其中就是用了 asyncawait 模式。 public async Taskint FindPageSizeAsync Uri uri var client new HttpClient byte data await clientGetByteArrayAsync uri return dataLength 為了便於理解，我們將上面的這一函式稱為主函式。注意下文中的 Promise 是一個概念上統稱，而不一定是具體存在的一種物件。 當函式被 async 關鍵字標記時，它會被編譯器認為是非同步的。這樣的函式中可能會有若干個 await 表示式，它們可將結果繫結到 Promise 物件上。主函式就是一個典型的非同步函式。 主函式的返回型別 TaskT（其中 T 為泛型）是 C 對 Promise 概念的一種實現，在程式碼中 Taskint 表明該 Promise 對應的實際結果是 int 型別的。 當主函式被執行時，首先一個新的 HttpClient 例項會被賦給 client。 接下來 await 後緊跟的一句表示式執行了 client 上的非同步方法 GetByteArrayAsync uri，它會返回一個 Taskbyte 。由於該方法是非同步的，它並不會等到下載完成才返回，而是以某種非阻塞的方式（例如後臺程序）開始下載，然後馬上返回一個沒有被 resolve 也沒有被 reject 的 Taskbyte 到主函式中。在這裡，resolve 和 reject 可以理解為 Task 物件上的兩個自帶的方法，resolve x 表示執行完成，reject x 表示執行失敗，二者都表示執行結束，因而都可用於最終的傳值。 由於該表示式返回的 Taskbyte 前有 await，接下來主函式會直接返回一個類似於之前 Task 物件的 Taskint 到其呼叫者。顯然，其呼叫者並不會被阻塞。 當 GetByteArrayAsync uri 下載結束後，它會使用其下載的資料 resolve 它所返回的那個 Task，即上文中的 Taskbyte。Resolve 會觸發一個回撥函式，使得主函式繼續向下執行 return dataLength。 然後與 GetByteArrayAsync uri 的行為類似，主函式也會使用 return 語句返回的值來 resolve 它所返回的 Taskint，觸發一個回撥函式，使得其呼叫者能夠開始使用這一具體值。 非同步函式內部可以根據需要使用多個 await 語句，每一個語句都會以相同的方式進行處理（實際上只有第一個 await 語句會返回 Promise，其餘的 await 都用機制類似的內部回撥函式實現）。對於返回的 Promise 物件，演算法中亦可以對其直接進行處理（例如先儲存起來），從而實現先執行其它任務（包括觸發新的非同步 Task），等到需要相關結果的時候才使用 await 語句處理 Promise 物件，拿到結果。 除了直接 await 之外，也有一些可以批次處理 Promise 物件的函式，比如 C 中的 TaskWhenAll 函式，它會返回一個 Task（無值 Task，可以理解為 Taskvoid）。這個被返回的 Task 會在 TaskWhenAll 方法引數中提供的所有的 Promise 被 resolve 以後 resolve。還有一些 Promise 返回型別支援通常 asyncawait 模式不會用到的一些方法，例如對 Promise 設定多個結果的回撥函式、監聽長耗時 Task 的執行程序等。 在 C 和許多其它語言中，asyncawait 模式並不是執行時的核心組成部分，而實際上會在編譯的時候使用 Lambda 表示式或者續體來實現。例如上面的 C 程式碼很可能會被編譯器先轉換成下面的程式碼，然後才被轉換成位元組碼。 public Taskint FindPageSizeAsync Uri uri var client new HttpClient Taskbyte dataTask clientGetByteArrayAsync uri Taskint afterDataTask dataTaskContinueWith originalTask return originalTaskResultLength return afterDataTask 也正因此，如果某個函式需要返回一個 Promise 物件，但是其本身並不需要進行任何的 await 求值，那麼它並不需要在函式宣告前面加上 async 來讓自己成為一個非同步函式，而是可以直接返回一個 Promise 物件。例如，使用 C 的 TaskFromResult 方法來返回一個馬上 resolve 的 Task，或者直接如同地鐵換乘一樣將其它函式所提供的 Task 直接原樣返回。 不過對於這項功能必須要注意的一項是，儘管在非同步函式內部的邏輯長得很像同步的形式，這些邏輯實際上是非阻塞的，甚至可能是多執行緒的。所以在 await 語句等待 Promise 被 resolve 的時候，可能會發生許多侵入性的事件。比如下面的程式碼，如果沒有 await，那麼將始終執行成功，但是如果使用了 asyncawait 模式，就可能會出現 statea 發生改變（被其它邏輯）的情況。 var a statea var client new HttpClient 與 a 無關的語句 var data await clientGetByteArrayAsync uri 與 a 無關的語句 DebugAssert a statea 這個語句可能會出現錯誤，因為 statea 可能在 await 的過程中被其它邏輯篡改。 return dataLength 在 F 中的使用 F 中的非同步邏輯的具體實現是計算表示式。具體使用時並不需要加上特殊的識別符號，例如 async。在程式碼邏輯中，使用一個感嘆號來開始非同步操作。 從 URL 下載資料的非同步函式邏輯如下： let asyncSumPageSizes uris seqUri Asyncint async use httpClient new HttpClient let pages uris Seqmap httpClientGetStringAsync AsyncAwaitTask AsyncParallel return pages Seqfold fun accumulator current currentLength accumulator 0 在 C 中的使用 微軟將 C 中的 asyncawait 模式稱作 以任務為基礎的非同步模式（Taskbased Asynchronous Pattern，TAP）。非同步函式的返回值通常包括 void、Task 、TaskT、 ValueTask 和 ValueTaskT。程式碼中還可以利用非同步函式構造器（async method builders）自行定義非同步函式的返回值型別，不過這一場景高階且少見。返回 void 的非同步函式通常應該是事件監聽器，對於一般的函式則應該返回 Task 物件，因為它能夠提供更加直觀的異常處理。Stephen Cleary AsyncAwait Best Practices in Asynchronous Programming 要在函式中使用 await，必須在函式宣告前加上 async 關鍵字。當需要函式返回 TaskT 型別的值時，函式宣告前要加上 async 關鍵字，同時應當返回 T 或相容的型別，而非 TaskT 本身；隨後編譯器就會將返回的 T 型別包裝為 TaskT 泛型。不過，當非非同步函式（沒有使用 async 宣告的函式）返回 TaskT 時，其值也可以被 await。 下列函式程式碼將使用 await 從 URL 下載資料。該函式的邏輯用 await 實現了同時觸發多個任務，無需等待其完成，這使得下一個任務無需在上一個任務完成之後才觸發（這是同步的邏輯）。 public async Taskint SumPageSizesAsync IEnumerableUri uris var client new HttpClient int total 0 var loadUriTasks new ListTaskbyte foreach var uri in uris var loadUriTask clientGetByteArrayAsync uri loadUriTasksAdd loadUriTask foreach var loadUriTask in loadUriTasks statusTextText 已找到 total 個位元組 var resourceAsBytes await loadUriTask total resourceAsBytesLength statusTextText 共找到 total 個位元組。 return total 在 Scala 中的使用 Scala 中有一個實驗性的拓展 Scalaasync 可以實現 asyncawait 模式。它提供了一個名為 await 的特殊函式。與 C 不同的是，Scala 中的非同步邏輯並不需要用 async 來標記。透過 Scalaasync，可以直接將非同步邏輯用 async 函式呼叫的形式包圍起來。 這是如何實現的 Scalaasync 所提供的 async 實際上是透過宏來實現的。編譯器會呼叫不同的程式碼，然後產生一個有限狀態機（通常認為這比單子實現更高效，但是更難以編寫）。 在 Python 中的使用 在 Python 中的使用，在語法上與 C、JavaScript 等類似。 import asyncio async def main print hello await asynciosleep 1 print world asynciorun main 在 JavaScript 中的使用 JavaScript 中的 await 運算子只能用於 async 標註的函式中，或者用於模組的最頂層。 如果 await 運算子後跟引數為 Promise 物件，那麼函式邏輯會在該 Promise 物件被 resolve 之後繼續執行，或者在它被 reject 以後丟擲異常（可以進行異常處理）；如果 await 運算子後跟引數不是 Promise 物件，那麼該值會被直接返回（不會等待）。 許多 JavaScript 庫提供了可返回 Promise 物件的函式，它們都可以被 await 只要符合 JavaScript 中的 Promise 規範。JQuery 中函式返回的 Promise 在 30 版本以後才達到了 PromisesA 相容度。 下面是一個使用例： async function createNewDoc let response await dbpost post a new doc return dbget responseid find by id async function main try let doc await createNewDoc consolelog doc catch err consolelog err main Nodejs 8 中包含的一樣工具可將標準庫中利用回撥模式編寫的函式當作 Promise 來使用。 在 C 中的使用 C 20 中正式支援了 await（在 C 中是 co_await）。GCC、MSVC 編譯器支援 asyncawait 模式，包括協程以及相關的關鍵字例如 co_await。Clang 對此有部分支援。 值得注意的是 stdpromise 和 stdfuture 雖然看起來像是可以被 await 求值的物件，但是實際上它們並沒有實現任何類似於從協程中返回的值或者可被 await 的物件的相關屬性；要使返回物件可以被 await 求值，必須在返回的物件型別上實現一系列的公共成員函式，例如 await_ready、await_suspend 和 await_resume 等。具體細節可以檢視相關的參考。 include iostream include CustomAwaitableTaskh using namespace std CustomAwaitableTaskint add int a int b int c a b co_return c CustomAwaitableTaskint test int ret co_await add 1 2 cout return ret endl co_return ret int main auto task test return 0 在 C 語言中的使用 C 語言沒有對 awaitasync 的官方支援。 某些協程庫（例如 s_task ）透過宏定義的方式， 實現了和其他語言類似的 awaitasync 的強制性語義要求，即： 1 必須在 async 標註的函式內，才能呼叫 await； 2 等待一個標註為 aysnc 的函式，呼叫該函式時需要加上 await； include stdioh include s_taskh 定義協程任務需要的棧空間 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 intsize_t arg for i 0 i 5 i printf task d delay seconds d i dn 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 dn 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 voidsize_t argc s_task_join __await__ g_stack_main printf all task is overn return 0 參考文獻 Category 控制流程