摘要:C++20 引入的協程機制為異步編程提供了輕量級解決方案,其核心優勢在于通過用戶態調度實現高效的上下文切換,適用于 I/O 密集型任務、生成器模式等場景。本文系統闡述 C++20 協程的底層原理與實踐要點,首先解析協程的基本結構,包括promise_type狀態控制器、coroutine_handle句柄及co_await、co_yield、co_return關鍵字的工作機制,揭示協程啟動、暫停、恢復與終止的完整生命周期。其次,深入探討協程的實現細節,如 Awaitable 與 Awaiter 對象的轉換邏輯、協程幀的內存管理(分配與釋放)、編譯器對協程的狀態機轉換(基于暫停點索引的執行控制)。針對協程使用中的關鍵問題,分析內存分配優化策略(自定義分配器與內存池)、協程與線程的本質區別(用戶態調度 vs 內核態調度),以及對稱轉移導致的棧溢出風險及解決方案(尾調用優化與std::noop_coroutine)。本文通過實例與偽代碼還原協程的編譯器轉換過程,為開發者理解協程機制、規避常見問題提供理論與實踐參考。
關鍵詞:C++20;協程;異步編程;promise_type;上下文切換
1 協程簡介
??C++20 引入了協程(Coroutines)支持。協程一種能夠暫停和恢復執行的特殊函數,非常適合處理異步操作、生成器、狀態機等場景,相比于通過回調函數、線程等方式,協程能夠更清晰地組織代碼,減少上下文切換的開銷,提高代碼的可讀性和可維護性。
1.1 協程和函數
??在 C++ 中協程是一種特殊的函數調用,它與普通函數存在顯著區別。普通函數一旦開始執行,會一直運行到返回語句或函數結束,期間無法暫停;而協程在執行過程中可以在特定點暫停,保存當前的執行狀態(包括局部變量、指令指針等),之后在合適的時機可以從暫停點恢復執行,繼續完成后續操作。
??從執行流程來看,普通函數的調用是一種棧式的調用方式,遵循 “先進后出” 的原則,每次調用都會在調用棧上分配新的棧幀;協程則擁有自己獨立的狀態存儲,其暫停和恢復不依賴于傳統的調用棧,這使得協程的上下文切換成本遠低于線程切換。
??簡單回顧下普通函數的調用流程,一個普通的函數調用流程如下:
- 函數調用(call):程序將控制權轉移給被調用函數,同時將返回地址壓入調用棧。
- 函數執行(execute):被調用函數開始執行,它會使用棧幀來存儲局部變量、參數、返回值等。
- 函數返回(return):被調用函數執行完畢,將返回值壓入棧幀,然后將控制權返回給調用函數。
- 調用棧彈出(pop):調用函數從調用棧中彈出返回地址,恢復控制權。
??其中,函數調用時會保存當前的執行狀態(包括局部變量、指令指針等),并將控制權交給被調用函數。當被調用函數執行完畢后,會從調用棧中彈出返回地址,恢復調用函數的執行狀態,繼續執行后續操作。也就是說函數一旦被調用就沒有回頭路,直到函數調用結束才會將控制權還給調用函數。
??而相比之下協程的調用路程要復雜的多:
- 協程啟動(start):當協程被調用時,系統會為其分配獨立的狀態存儲(不同于傳統棧幀),用于保存局部變量、指令指針、寄存器狀態等信息。此時協程進入初始狀態,可根據其返回類型的
initial_suspend
策略決定是否立即執行或暫停。 - 協程執行(execute):協程開始執行,與普通函數類似,但在遇到
co_await
、co_yield
等關鍵字時會觸發暫停邏輯。- 若執行
co_await expr
,協程會先計算表達式expr
得到一個 “等待體”(awaitable),然后調用該等待體的awit_suspend
方法。若該方法返回true或一個協程句柄,當前協程會暫停,將控制權交還給調用者或切換到其他協程;若返回false,則繼續執行。 - 若執行
co_yield expr
,本質是通過特殊的等待體暫停協程,并向調用者返回一個值,之后可從暫停點恢復。
- 若執行
- 協程暫停(suspend):協程暫停時,其當前執行狀態被完整保存到獨立存儲中,調用棧不會被銷毀。此時控制權返回給調用者或調度器,調用者可繼續執行其他任務,或在合適時機恢復協程。
- 協程恢復(resume):調用者通過協程句柄的
resume()
方法觸發協程恢復。系統從保存的狀態中還原執行環境,協程從暫停點繼續執行后續代碼。 - 協程結束(finalize):當協程執行到
co_return
語句或函數末尾時,會觸發final_suspend
策略。通常此時協程會進入最終暫停狀態,等待調用者通過句柄銷毀其狀態存儲,以釋放資源。
??這種流程使得協程能夠在執行過程中多次暫停和恢復,且每次切換僅涉及狀態存儲的讀寫,無需像線程那樣切換內核態上下文,因此效率更高。同時,協程的狀態保持特性讓異步操作的代碼編寫更接近同步邏輯,大幅降低了回調嵌套帶來的復雜性。
1.2 簡單的協程
??下面是一個簡單的 C++20 協程示例,展示了協程的基本使用方式:
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
#include <spdlog/spdlog.h>struct SimpleCorontinePromise;
struct SimpleCorontine {using promise_type = SimpleCorontinePromise;
};struct SimpleCorontinePromise {SimpleCorontine get_return_object() {SPDLOG_INFO("get_return_object");return {};}void return_void() {SPDLOG_INFO("return_void");}std::suspend_never initial_suspend() noexcept {SPDLOG_INFO("initial_suspend");return {};}std::suspend_never final_suspend() noexcept {SPDLOG_INFO("final_suspend");return {};}void unhandled_exception() {SPDLOG_INFO("unhandled_exception");}
};SimpleCorontine MySimpleCorontine() {SPDLOG_INFO("Corontine Start");co_return;SPDLOG_INFO("Corontine End");
}int testSimpleCorontine() {SPDLOG_INFO("Main thread started executing 1");auto coro = MySimpleCorontine(); // Ensure the lifecycle of coro is managedSPDLOG_INFO("Main thread started executing 2");return 0;
}
??上面的代碼定義了一個簡單的協程MySimpleCorontine
,其包括:
- 協程函數
MySimpleCorontine
,用于定義協程的執行邏輯。 - 協程Promise類型
SimpleCorontinePromise
,用于定義協程的狀態管理和返回值處理。Promise
必須- 定義
get_return_object()
方法,用于返回協程對象。 - 定義
return_void()
方法,用于處理協程函數執行完畢后的邏輯。 - 定義
initial_suspend()
方法,用于定義協程的初始暫停策略。 - 定義
final_suspend()
方法,用于定義協程的最終暫停策略。 - 定義
unhandled_exception()
方法,用于處理協程執行過程中發生的異常。
- 定義
- 協程類型
SimpleCorontine
,用于表示協程對象。而SimpleCorontine
必須定義promise_type
成員,用于指定Promise類型。
在協程的第一行調用
get_return_object
是為了確保返回對象的有效性、管理狀態和資源、提供異常安全性、簡化控制流以及滿足編譯器設計的需要。
??其執行結果如下:
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:44] Main thread started executing 1
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:14] get_return_object
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:23] initial_suspend
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:38] Corontine Start
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:19] return_void
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:28] final_suspend
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:47] Main thread started executing 2
??從執行結果,我們可以整理出其基本的調用流程,能夠注意到co_return
之后的代碼不會被執行,這是因為co_return
會觸發final_suspend
策略,導致協程進入最終暫停狀態。
1.3 協程resume
??上面的代碼中,我們并沒有控制協程的調用流程,似乎協程只是按照某種約定按照順序調用規定的函數(雖然事實也是如此)。我們嘗試將代碼修改成下面的樣子,通過resume
來控制協程的調用流程。改動如下,完整的代碼見
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
#include <spdlog/spdlog.h>struct SimpleCoroutinePromise;struct SimpleCoroutine {using promise_type = SimpleCoroutinePromise;std::coroutine_handle<promise_type> handle;SimpleCoroutine(std::coroutine_handle<promise_type> handle) : handle(handle) {}SimpleCoroutine(const SimpleCoroutine&) = delete;SimpleCoroutine& operator=(const SimpleCoroutine&) = delete;SimpleCoroutine(SimpleCoroutine&& other) noexcept : handle(other.handle) {other.handle = nullptr;}SimpleCoroutine& operator=(SimpleCoroutine&& other) noexcept {if (this != &other) {if (handle) {handle.destroy(); // Destroy the current handle if it exists}handle = other.handle;other.handle = nullptr;}return *this;}void resume() {if (handle) {handle.resume();}}~SimpleCoroutine() {if (handle) {handle.destroy();}}
};struct SimpleCoroutinePromise {SimpleCoroutine get_return_object() {SPDLOG_INFO("get_return_object");return SimpleCoroutine(std::coroutine_handle<SimpleCoroutinePromise>::from_promise(*this));}void return_void() {SPDLOG_INFO("return_void");}std::suspend_always initial_suspend() noexcept {SPDLOG_INFO("initial_suspend");return {};}std::suspend_always final_suspend() noexcept {SPDLOG_INFO("final_suspend");return {};}void unhandled_exception() {SPDLOG_INFO("unhandled_exception");}
};SimpleCoroutine MySimpleCoroutine() {SPDLOG_INFO("Coroutine Start");co_return; // This will directly return, and the coroutine ends hereSPDLOG_INFO("Coroutine End"); // This line will not be executed
}int testSimpleCorontine() {SPDLOG_INFO("Main thread started executing 1");auto coro = MySimpleCoroutine();SPDLOG_INFO("Main thread started executing 2");coro.resume();SPDLOG_INFO("Main thread started executing 3");return 0;
}
??上面的代碼輸出結果為:
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:78] Main thread started executing 1
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:48] get_return_object
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:57] initial_suspend
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:80] Main thread started executing 2
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:72] Coroutine Start
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:53] return_void
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:62] final_suspend
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:82] Main thread started executing 3
??上面的輸出相比之前的輸出能夠發現,執行完協程的初始化相關函數之后,協程就講控制權交給了主函數,主函數通過resume
來恢復協程的執行,之后再執行協程相關的代碼。相關的改動:
- 創建一個 handle 來管理協程的生命周期。這個 handle 允許我們控制何時恢復協程的執行以及最終何時銷毀它。
- 修改suspend狀態,其中:
suspend_always
:協程在達到指定的掛起點(如initial_suspend()
)時會暫時停止執行,并將控制權返回給調用者。調用者隨后可以選擇何時恢復協程的執行。suspend_never
:協程在到達掛起點時直接繼續執行,控制權不會返回給調用者。這意味著協程會在達到終點后直接終止,而不會暫停。
1.4 協程yield
??除了基本的結構,協程還有其他的功能,比如yield
。co_yield
是 C++20 協程中用于 “產出值并暫停” 的關鍵字,主要用于實現生成器(Generator)模式,允許協程在執行過程中多次返回值,每次返回后暫停,等待下次被恢復時繼續執行。co_yield
本質上是一種語法糖,等價于co_await promise.yield_value(expr)
。其工作流程如下:
- 當協程執行到
co_yield expr
時,首先計算表達式expr
的值; - 將該值存儲到
promise_type
中,供調用者獲取; - 協程暫停執行,將控制權交還給調用者;
- 當調用者通過協程句柄恢復協程時,協程從 co_yield 之后的代碼繼續執行。
??基于此,我們可以實現一個簡單的generator用來生成數字。
struct Generator {struct GeneratorPromise {using Handle = std::coroutine_handle<GeneratorPromise>;Generator get_return_object() {return Generator(Handle::from_promise(*this));}std::suspend_always initial_suspend() noexcept {return {};}std::suspend_always final_suspend() noexcept {return {};}void return_void() {}void unhandled_exception() {}std::suspend_always yield_value(int v) {value = v;return {};}int value{};};using promise_type = GeneratorPromise;using Handle = std::coroutine_handle<promise_type>;Generator(Handle handle) : handle(handle) {}~Generator() {if (handle) {handle.destroy();}}Generator(const Generator&) = delete;Generator& operator=(const Generator&) = delete;Generator(Generator&& other) noexcept : handle(other.handle) {other.handle = nullptr;}Generator& operator=(Generator&& other) noexcept {if (this != &other) {if (handle) {handle.destroy();}handle = other.handle;other.handle = nullptr;}return *this;}bool done(){return handle.done();}int next(){if(done()){return - 1;}handle.resume();if (done()) {return - 1;}return handle.promise().value;}Handle handle;
};Generator GeneratorNum(){for(int i = 0;i < 5;i ++){co_yield i;}
}int testSimpleGenerator() {auto gen = GeneratorNum();while(!gen.done()){SPDLOG_INFO("num {}", gen.next());}return 0;
}
??代碼的輸出為:
[2025-07-23 22:50:28.006] [info] [SimpleCorontine.cpp:165] num 0
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 1
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 2
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 3
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 4
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num -1
1.5 協程co_return
??co_return 是用于終止協程執行并返回結果的關鍵字,類似于普通函數中的 return,但專門針對協程的特性設計,用于結束協程的生命周期并傳遞最終結果。co_return 的語法有兩種形式:
- 無返回值:co_return。用于不需要返回最終結果的協程,僅表示協程執行結束。
- 帶返回值:co_return 表達式。用于需要向調用者返回最終結果的協程,表達式的值會被傳遞給協程的
promise_type
(promise_type)。
??當協程執行到co_return
時,會觸發以下流程:
- 處理返回值:
- 若為
co_return expr
;,則表達式expr
的值會被傳遞給協程promise_type
的return_value(expr)
方法(需在promise_type
中定義),由promise_type
存儲該結果,供調用者獲取。 - 若為
co_return
;,則調用promise_type
的return_void()
方法(無返回值場景)。
- 若為
- 觸發最終掛起:
- 協程執行完返回值處理后,會調用
promise_type
的final_suspend()
方法,根據其返回的掛起策略(std::suspend_always
或std::suspend_never
)決定是否暫停。 - 通常會返回
std::suspend_always
,讓協程進入最終暫停狀態,等待調用者通過協程句柄銷毀資源。
- 協程執行完返回值處理后,會調用
- 協程終止:協程進入最終暫停狀態后,其生命周期并未完全結束,需等待調用者顯式調用
coroutine_handle::destroy()
釋放協程占用的資源(如狀態存儲、局部變量等)。
??之前的協程例子是不帶返回值的,這里通過reutrn_value
來處理返回值。
// 帶返回值的協程返回類型
struct ResultTask {struct promise_type {std::string result; // 存儲協程的返回結果// 獲取協程返回對象ResultTask get_return_object() {return ResultTask{std::coroutine_handle<promise_type>::from_promise(*this)};}// 初始掛起:立即執行std::suspend_never initial_suspend() { return {}; }// 最終掛起:暫停以等待銷毀std::suspend_always final_suspend() noexcept { return {}; }// 處理帶值的 co_returnvoid return_value(const std::string& val) {result = val; // 保存返回值}// 異常處理void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;// 提供接口讓調用者獲取返回值std::string get_result() const {return handle.promise().result;}
};// 示例協程:執行一些操作后返回結果
ResultTask process_data(int input) {SPDLOG_INFO("process_data");SPDLOG_INFO("input {}", input);// 模擬一些處理邏輯if (input < 0) {co_return "error, the input is negative"; // 返回錯誤信息}int result = input * 2;co_return "process data done: " + std::to_string(result); // 返回計算結果
}int testResultTask() {// 啟動協程auto task = process_data(10);SPDLOG_INFO("task handle done {}", task.handle.done());// 檢查協程是否完成if (task.handle.done()) {SPDLOG_INFO("task handle done {}", task.handle.done());SPDLOG_INFO("task result {}", task.get_result());}// 釋放協程資源task.handle.destroy();return 0;
}
??上述代碼的輸出結果是:
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:209] process_data
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:210] input 10
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:224] task handle done true
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:228] task handle done true
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:229] task result process data done: 20
1.6 協程await
??co_await
是核心關鍵字之一,用于 “等待一個異步操作完成”,并在等待期間暫停當前協程,將控制權交還給調用者或調度器。當被等待的操作完成后,協程可以從暫停點恢復執行。co_await
后跟一個 “可等待對象”(awaitable),語法形式為:
co_await 可等待對象;
??“可等待對象” 是指實現了特定接口(3 個核心方法)的對象,它代表一個可能尚未完成的異步操作(如網絡請求、文件 I/O 等)。一個對象要能被 co_await 等待,必須滿足以下條件(或通過適配器轉換后滿足):
await_ready()
:判斷操作是否已完成。- 返回 true:操作已完成,co_await 不暫停,直接繼續執行。
- 返回 false:操作未完成,co_await 會暫停協程。
await_suspend(handle)
:當操作未完成時調用,負責注冊 “喚醒回調”。- 參數 handle 是當前協程的句柄(std::coroutine_handle)。
- 返回值決定后續行為:
- 返回 void:暫停當前協程,控制權返回給調用者。
- 返回 false:不暫停,繼續執行當前協程。
- 返回另一個協程句柄:切換到該協程執行。
await_resume()
:當操作完成、協程恢復時調用,返回異步操作的結果(或拋出異常)。
??下面是一個簡單的通過協程await
異步等待的例子:
struct AsyncTimer {bool await_ready() {SPDLOG_INFO("await_ready");return false;}void await_suspend(std::coroutine_handle<> handle) {SPDLOG_INFO("await_suspend");std::thread([this, handle]() {std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));handle.resume(); // 延遲結束,恢復協程}).detach();}void await_resume() const noexcept {SPDLOG_INFO("await_resume");}int delay_ms;
};struct Task {struct promise_type {Task get_return_object() {SPDLOG_INFO("get_return_object");return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_never initial_suspend() { SPDLOG_INFO("initial_suspend");return {}; } // 立即執行std::suspend_always final_suspend() noexcept { SPDLOG_INFO("final_suspend");return {}; } // 最終暫停void return_void() {SPDLOG_INFO("return_void");}void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;
};Task async_task() {SPDLOG_INFO("Task start");SPDLOG_INFO("wait 1 seconds........");co_await AsyncTimer{ 1000 }; // 等待1秒(異步操作)SPDLOG_INFO("wait 1 second done");SPDLOG_INFO("wait another 1 seconds........");co_await AsyncTimer{ 2000 }; // 再等待2秒SPDLOG_INFO("wait 2 second done");
}int testAsyncTask() {auto task = async_task();// 等待協程完成(簡化處理,實際需更復雜的調度)SPDLOG_INFO("wait task done........");while (!task.handle.done()) {std::this_thread::sleep_for(std::chrono::milliseconds(100));}SPDLOG_INFO("task done");task.handle.destroy(); // 釋放資源return 0;
}
??上面的程序輸出為,能夠看到每次協程等待都會調用對應的await_ready
、await_suspend
、await_resume
方法。并且我們的例子中沒有添加co_return
那是因為我們的例子不需要返回值,如果實際上需要的話還是要加上對應的co_return
。
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:262] get_return_object
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:266] initial_suspend
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:283] Task start
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:284] wait 1 seconds........
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:239] await_ready
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:244] await_suspend
[2025-07-24 08:56:34.289] [info] [SimpleCorontine.cpp:296] wait task done........
[2025-07-24 08:56:35.310] [info] [SimpleCorontine.cpp:252] await_resume
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:286] wait 1 second done
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:288] wait another 1 seconds........
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:239] await_ready
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:244] await_suspend
[2025-07-24 08:56:37.326] [info] [SimpleCorontine.cpp:252] await_resume
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:290] wait 2 second done
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:274] return_void
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:270] final_suspend
[2025-07-24 08:56:37.356] [info] [SimpleCorontine.cpp:301] task done
2 深入理解協程
??上面談到了協程的基本原理,但是協程的實現原理是比較復雜的,上面的例子只是一個簡單的例子,實際上協程的實現原理是基于狀態機的,每個協程在不同的狀態下會調用不同的方法,并且協程的狀態是可以切換的。協程的狀態機模型使得協程能夠在執行過程中掛起和恢復。每個協程都有一個內部狀態,指示其當前執行位置。狀態可以包括:
- 初始狀態:協程剛被創建,尚未開始執行。
- 掛起狀態:協程執行到
co_await
或co_yield
時掛起,等待某個事件或值。 - 完成狀態:協程執行結束,所有操作完成。
??狀態機的狀態切換主要通過co_await
,co_yield
和resume
等操作配合控制。而為了更加精細的控制協程,C++20提供了promise_type
,promise_type
是協程中用于管理狀態和結果的核心組件,可以讓我們控制協程掛起,暫停,完成等狀態切換時的動作。其中co_await
和promise_type
相對比較復雜,下面就展開描述下。
2.2 協程句柄
??協程句柄(coroutine handle)是一個指向協程的特殊對象,允許開發者控制協程的執行狀態。它提供了一種機制,用于管理和恢復協程的執行。句柄能夠用到的關鍵方法有resume,destroy,promise
分別用來恢復協程,銷毀協程和獲取promise對象用來和協程交互。協程句柄大致的接口如下:
namespace std::experimental
{template<typename Promise>struct coroutine_handle;template<>struct coroutine_handle<void>{bool done() const;void resume();void destroy();void* address() const;static coroutine_handle from_address(void* address);};template<typename Promise>struct coroutine_handle : coroutine_handle<void>{Promise& promise() const;static coroutine_handle from_promise(Promise& promise);static coroutine_handle from_address(void* address);};}
2.2 co_await
??co_await
用于在協程中等待某個操作完成,它的作用是暫停協程的執行,等待操作完成后再恢復協程的執行。co_await
后面的表達式需要是一個Awaitable
對象。需要注意的是C++20實現中為了提高靈活性、可重用性和性能,將co_await
接受的對象分為了Awaitable
對象和Awaiter
。
Awaitable
:其類型實現了特定的接口,使其能與 co_await 關鍵字一起使用。Awaitable 對象能夠在協程中被掛起,并在異步操作完成后恢復。如果運算符重載了operator co_await
,當表達式使用 co_await 時,會嘗試調用operator co_await
。Awaiter
:實現了await_ready
、await_suspend
、await_resume
三個方法的對象。Awaiter
是一個與Awaitable
相關的對象,負責處理協程的掛起和恢復邏輯。Awaiter
提供了方法來管理協程的執行狀態。
??執行co_await expr
表達式時,首先將expr
轉換成一個Awaitable
對象,然后轉換成Awaiter
:
- 構建
Awaitable
對象- 如果表達式是由初始掛起點、最終掛起點或
yield
表達式產生的,Awaitable
就是該表達式本身。 - 如果當前協程的
Promise
類型具有await_transform
成員函數,Awaitable
將是promise.await_transform(expr)
的結果。 - 如果不滿足以上條件,
Awaitable
就是該表達式本身。
- 如果表達式是由初始掛起點、最終掛起點或
- 構建
Awaiter
對象。根據Awaitable
對象構造Awaiter
對象。- 如果
operator co_await
的重載解析為單一最佳重載,Awaiter
就是該調用的結果。- 對于成員重載,使用
awaitable.operator co_await()
。 - 對于非成員重載,使用
operator co_await(static_cast<Awaitable&&>(awaitable))
。
- 對于成員重載,使用
- 如果沒有找到合適的重載,
Awaiter
就是Awaitable
本身。
- 如果
??上述過程大致偽代碼如下:
template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{if constexpr (has_any_await_transform_member_v<P>)return promise.await_transform(static_cast<T&&>(expr));elsereturn static_cast<T&&>(expr);
}template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{if constexpr (has_member_operator_co_await_v<Awaitable>)return static_cast<Awaitable&&>(awaitable).operator co_await();else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)return operator co_await(static_cast<Awaitable&&>(awaitable));elsereturn static_cast<Awaitable&&>(awaitable);
}
??獲取Awaiter
之后就可以根據其定義的await_suspend
等實現來對協程進行控制。
{auto&& value = <expr>;auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));if (!awaiter.await_ready()){using handle_t = std::experimental::coroutine_handle<P>;using await_suspend_result_t =decltype(awaiter.await_suspend(handle_t::from_promise(promise)));<suspend-coroutine>if constexpr (std::is_void_v<await_suspend_result_t>){awaiter.await_suspend(handle_t::from_promise(promise));<return-to-caller-or-resumer>}else{static_assert(std::is_same_v<await_suspend_result_t, bool>,"await_suspend() must return 'void' or 'bool'.");if (awaiter.await_suspend(handle_t::from_promise(promise))){<return-to-caller-or-resumer>}}<resume-point>}return awaiter.await_resume();
}
??下面寫一個簡單的例子來展示Awaitable
和Awaiter
對象構造過程。
class MyAwaiter {
public:bool await_ready() const noexcept {SPDLOG_INFO("Awaiter: Checking if ready");return false; }void await_suspend(std::coroutine_handle<>) {SPDLOG_INFO("Awaiter: Coroutine suspended");}int await_resume() {SPDLOG_INFO("Awaiter: Resuming coroutine");return 42; }
};// Awaitable 類
class MyAwaitable {
public:MyAwaitable(std::string v) {SPDLOG_INFO("MyAwaitable::MyAwaitable");}MyAwaiter operator co_await() {SPDLOG_INFO("Awaitable: Co-await called");return MyAwaiter(); // 返回 Awaiter 對象}
};// 協程示例
struct MyCoroutine {struct promise_type {MyCoroutine get_return_object() {SPDLOG_INFO("get_return_object");return MyCoroutine{};}auto initial_suspend() noexcept {SPDLOG_INFO("initial_suspend");return std::suspend_never{};}auto final_suspend() noexcept {SPDLOG_INFO("final_suspend");return std::suspend_never{};}void return_void() {}void unhandled_exception() {}template <typename T>auto await_transform(T expr) {SPDLOG_INFO("Awaitable: await_transform called");return MyAwaitable(""); // 返回 Awaiter 對象}};
};MyCoroutine start() {SPDLOG_INFO("Coroutine started");co_await ""; // 使用 AwaitableSPDLOG_INFO("Coroutine resumed");co_return;
}void testAwaiter(){SPDLOG_INFO("testAwaiter started");auto corn = start();SPDLOG_INFO("testAwaiter end");
}
??上面的代碼輸出如下,和上面描述的流程完全一致。
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:371] testAwaiter started
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:341] get_return_object
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:345] initial_suspend
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:364] Coroutine started
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:357] Awaitable: await_transform called
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:328] MyAwaitable::MyAwaitable
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:332] Awaitable: Co-await called
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:310] Awaiter: Checking if ready
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:315] Awaiter: Coroutine suspended
[2025-07-24 22:57:41.605] [info] [SimpleCorontine.cpp:373] testAwaiter end
??根據上面的流程可以看出,我們可以根據await
的參數來構造Awaitable
對象從而獲得Awaiter
,這樣就給我們控制協程流程提供了遍歷,我們可以通過Awaitable
對我們的邏輯進行封裝,可以不同情況使用不同的Awaiter
使得邏輯更加清晰,更加可擴展。
2.3 promise_type
??協程的另一個重點是promise_type
,promise_type
是一個協程狀態控制器,用于定義協程的行為,包括協程的返回值、異常處理、協程的掛起和恢復等。任何一個協程必須包含promise_type
,否則無法通過編譯。當我們實現了一個協程的promise_type
之后,其運行的大致流程如下:
{co_await promise.initial_suspend();try{<body-statements>}catch (...){promise.unhandled_exception();}FinalSuspend:co_await promise.final_suspend();
}
??編譯器決定promise_type
的類型,是根據coroutine_traits
來獲取的,我們可以通過下面方式獲取到對應協程的promise_typ
,需要注意的是協程的參數列表要和模板的列表對應上。
UserAllocCoroutine userAllocCoroutine(MyClass cls, MyClass cls2) {SPDLOG_INFO("Coroutine started.");co_return;SPDLOG_INFO("Coroutine resumed.");
}void testPromiseType(){using promise_type = std::coroutine_traits<UserAllocCoroutine, MyClass, MyClass>::promise_type;const auto name = std::string(typeid(promise_type).name());SPDLOG_INFO("promise type {}", name);
}
??上面只是大體的流程,實際的執行流程有很多細節:
- 使用
operator new
分配協程幀(可選,由編譯器實現)。 - 將所有函數參數復制到協程幀中。
- 調用類型為 P 的
promise_type
的構造函數。 - 調用
promise.get_return_object()
方法獲取協程首次暫停時返回給調用者的結果,并將其保存為局部變量。 - 調用
promise.initial_suspend()
方法并co_await
其結果。 - 當
co_await promise.initial_suspend()
表達式恢復執行(無論是立即恢復還是異步恢復)時,協程開始執行你編寫的函數體語句。 - 重復6步驟,直到執行到
co_return
:- 調用
promise.return_void()
或promise.return_value(<expr>)
。 - 按創建順序的逆序銷毀所有自動存儲期變量。
- 調用
promise.final_suspend()
并co_await
其結果。
- 調用
??當執行過程中發生未被捕獲的異常時,會觸發unhandled_exception
:
- 捕獲異常,并在 catch 塊中調用
promise.unhandled_exception()
。 - 調用
promise.final_suspend()
并co_await
其結果。
??從上面的流程能夠看出,協程的運行基本上都是通過promise_type
進行控制的。
2.4 協程幀
??函數的執行有棧幀來保存現場恢復現場,對應的協程有協程幀在執行時用來保存其局部狀態、局部變量、調用棧以及其他上下文信息的結構。和函數棧幀類似,協程幀也有其相關的創建和銷毀流程,相關時機自然不用說分別在協程的調用開頭和協程結束點。
協程幀創建和銷毀
??協程幀通過非數組形式的operator new
動態分配內存。如果Promise type
定義了類級別的 operator new
重載,則使用這個重載進行分配;否則,將使用全局 operator new
。但是需要注意的是傳遞給 operator new
的大小不是 sizeof(P)
,而是整個協程幀的大小。編譯器會根據參數數量和大小、promise_type
大小、局部變量數量和大小,以及管理協程狀態所需的其他編譯器特定存儲,自動計算該大小。同時若能確定協程幀的生命周期嚴格嵌套在調用者的生命周期內且在調用點能確定協程幀所需的大小,編譯器也會根據優化策略選擇省略operator new
調用。申請內存有可能會失敗,針對該情況,若promise_type
提供了靜態成員函數 P::get_return_object_on_allocation_failure()
,編譯器會轉而調用 operator new(size_t, nothrow_t)
重載。若該調用返回 nullptr
,協程會立即調用 P::get_return_object_on_allocation_failure()
,并將結果返回給調用者,而非拋出異常。
??下面的例子通過重載了operator new/delete
操作來hook創建協程幀的動作。
struct UserAllocCoroutine {struct UserAllocPromise {UserAllocPromise(){SPDLOG_INFO("UserAllocPromise constructed.");}// 自定義的 operator newvoid* operator new(std::size_t size) {SPDLOG_INFO("Custom operator new called, size: {} sizeof(UserAllocCoroutine) = {}", size, sizeof(UserAllocCoroutine));return ::operator new(size); // 調用全局 operator new}// 自定義的 operator deletevoid operator delete(void* ptr) noexcept {SPDLOG_INFO("Custom operator delete called.");::operator delete(ptr); // 調用全局 operator delete}// 協程返回對象auto get_return_object() {SPDLOG_INFO("get_return_object called.");return UserAllocCoroutine{ std::coroutine_handle<UserAllocPromise>::from_promise(*this) };}// 處理內存分配失敗static auto get_return_object_on_allocation_failure() {SPDLOG_INFO("Allocation failed, returning alternative object.");std::terminate();return UserAllocCoroutine{}; // 返回一個默認構造的協程}// 初始掛起auto initial_suspend() noexcept {SPDLOG_INFO("initial_suspend called.");return std::suspend_always{};}// 最終掛起auto final_suspend() noexcept {SPDLOG_INFO("final_suspend called.");return std::suspend_always{};}void return_void() {SPDLOG_INFO("return_void called.");}void unhandled_exception() {SPDLOG_INFO("unhandled_exception called.");std::exit(1);}};using promise_type = UserAllocPromise;std::coroutine_handle<UserAllocPromise> handle;UserAllocCoroutine(std::coroutine_handle<UserAllocPromise> h) : handle(h) {SPDLOG_INFO("UserAllocCoroutine constructed.");}UserAllocCoroutine() : handle(nullptr) {SPDLOG_INFO("UserAllocCoroutine default constructed.");}~UserAllocCoroutine() {if (handle) {SPDLOG_INFO("Destroying coroutine.");handle.destroy();}}
};// 協程函數
UserAllocCoroutine userAllocCoroutine() {SPDLOG_INFO("Coroutine started.");co_return;SPDLOG_INFO("Coroutine resumed.");
}int testUserAlloc() {spdlog::set_level(spdlog::level::info); // 設置日志級別auto coroutine = userAllocCoroutine(); // 啟動協程coroutine.handle.resume(); // 恢復協程return 0;
}
??對應的輸出如下,可以看到new/delete
分別是在協程開始和結束時調用的。同時能夠看到協程幀的大小。
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:385] Custom operator new called, size: 432 sizeof(UserAllocCoroutine) = 8
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:380] UserAllocPromise constructed.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:397] get_return_object called.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:434] UserAllocCoroutine constructed.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:410] initial_suspend called.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:451] Coroutine started.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:421] return_void called.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:416] final_suspend called.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:443] Destroying coroutine.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:391] Custom operator delete called.
參數復制到協程幀
??協程幀的參數復制規則和函數調用的參數復制規則類似,如果期望將參數傳遞給promise_type
,只需要在promise_type
構造函數中添加期望傳遞的參數即可。同時需要考慮參數的生命周期確保協程訪問期間其生命周期是確定的:
- 若參數按值傳遞,則通過調用該類型的移動構造函數將參數復制到協程幀。
- 若參數按引用傳遞(左值引用或右值引用),則僅將引用復制到協程幀,而非引用指向的值。
struct MyClass{MyClass() {SPDLOG_INFO("MyClass::MyClass");}std::string name = "";
};struct UserAllocCoroutine {struct UserAllocPromise {MyClass cls;UserAllocPromise(MyClass cls, MyClass cls2){SPDLOG_INFO("UserAllocPromise constructed.");cls = cls;}//省略部分代碼};
//省略部分代碼
};UserAllocCoroutine userAllocCoroutine(MyClass cls, MyClass cls2) {SPDLOG_INFO("Coroutine started.");co_return;SPDLOG_INFO("Coroutine resumed.");
}
2.5 更深入理解協程
??之前對于協程的不同操作符等進行了簡單的描述,為了更加深入理解協程的運作方式,本節將通過偽代碼來描述不同操作對應的等效代碼。假設有以下場景:
class task {
public:struct awaiter;class promise_type {public:promise_type() noexcept;~promise_type();struct final_awaiter {bool await_ready() noexcept;std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept;void await_resume() noexcept;};task get_return_object() noexcept;std::suspend_always initial_suspend() noexcept;final_awaiter final_suspend() noexcept;void unhandled_exception() noexcept;void return_value(int result) noexcept;private:friend task::awaiter;std::coroutine_handle<> continuation_;std::variant<std::monostate, int, std::exception_ptr> result_;};task(task&& t) noexcept;~task();task& operator=(task&& t) noexcept;struct awaiter {explicit awaiter(std::coroutine_handle<promise_type> h) noexcept;bool await_ready() noexcept;std::coroutine_handle<promise_type> await_suspend(std::coroutine_handle<> h) noexcept;int await_resume();private:std::coroutine_handle<promise_type> coro_;};awaiter operator co_await() && noexcept;private:explicit task(std::coroutine_handle<promise_type> h) noexcept;std::coroutine_handle<promise_type> coro_;
};task g(int x) {int fx = co_await f(x);co_return fx * fx;
}
????當編譯器發現函數包含三個協程關鍵字(co_await
、co_yield
或co_return
)中的任何一個時,就會開始協程轉換過程。其轉換的基本步驟如下面描述。
確定promise_type
??第一步是通過將簽名的返回類型和參數類型作為模板參數代入std::coroutine_traits類型來確定的promise_type。
using __g_promise_t = std::coroutine_traits<task, int>::promise_type;
創建協程state
??協程函數需要在暫停時保存協程的狀態、參數和局部變量,以便在后續恢復時仍可訪問。協程狀態包含以下幾部分:
promise_type
(promise object)- 所有函數參數的副本
- 關于當前暫停點的信息以及如何恢復 / 銷毀協程
- 生命周期跨越暫停點的局部變量 / 臨時對象的存儲
??上面提到過promise_type
的構造過程,編譯器會首先嘗試用參數副本的左值引用來調用promise_type
構造函數(如果有效),否則回退到調用promise_type
的默認構造函數。這里不再贅述,下面是一個簡單的輔助函數來描述該過程。
template<typename Promise, typename... Params>
Promise construct_promise([[maybe_unused]] Params&... params) {if constexpr (std::constructible_from<Promise, Params&...>) {return Promise(params...);} else {return Promise();}
}
??基于此,我們添加一個簡單的帶構造函數的__g_state
來描述協程狀態。
struct __g_state {__g_state(int&& x): x(static_cast<int&&>(x)), __promise(construct_promise<__g_promise_t>(this->x)){}int x;__g_promise_t __promise;// 待填充
};
??進入協程之后,救護創建協程state用來控制協程,如果沒有定義operator new
則直接走默認的全局new
,否則使用對應的重載,下面就是具體的過程。和之前描述的對齊,失敗時轉到get_return_object_on_allocation_failure
處理分配錯誤。
template<typename Promise, typename... Args>
void* __promise_allocate(std::size_t size, [[maybe_unused]] Args&... args) {if constexpr (requires { Promise::operator new(size, args...); }) {return Promise::operator new(size, args...);} else {return Promise::operator new(size);}
}task g(int x) {void* state_mem = __promise_allocate<__g_promise_t>(sizeof(__g_state), x);__g_state* state;try {state = ::new (state_mem) __g_state(static_cast<int&&>(x));if (state == nullptr) {return __g_promise_t::get_return_object_on_allocation_failure();}} catch (...) {__g_promise_t::operator delete(state_mem);throw;}// ... 實現啟動函數的其余部分
}
創建返回對象
??創建協程state之后就是調用get_return_object
獲取返回值,這個返回值被存儲為局部變量,并在啟動函數的最后(完成其他步驟后)返回。我們將上面偽代碼中的operator new
重載全部替換為全局new
來簡化邏輯,方便查閱。
task g(int x) {std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));decltype(auto) return_value = state->__promise.get_return_object();// ... 實現啟動函數的其余部分return return_value;
}
初始暫停點
??啟動函數在調用get_return_object()
之后要做的是開始執行協程體,而協程體中要執行的第一件事是初始暫停點,即求值co_await promise.initial_suspend()
。由于從initial_suspend()
和(可選的)operator co_await()
返回的對象的生命周期會跨越暫停點(它們在協程暫停之前創建,在恢復之后銷毀),這些對象的存儲需要放在協程狀態中。那考慮如果求值過程中發生了異常,那么:
- 以下情況發生的異常會傳播回啟動函數的調用者,并且協程狀態會被自動銷毀:
initial_suspend()
的調用- 對返回的可等待對象的
operator co_await()
調用(如果已定義) - 等待體的
await_ready()
調用 - 等待體的
await_suspend()
調用
- 以下場景發生的異常會被協程體捕獲,并調用
promise.unhandled_exception()
:await_resume()
的調用- 從
operator co_await()
返回的對象的析構函數(如適用) - 從
initial_suspend()
返回的對象的析構函數
??雖然上面的例子中初始化使用的initial_suspend()
返回的是std::suspend_always
,但是如果返回的其他可等待類型,那就有可能發生上面描述的情況。因此需要在協程狀態中為它保留存儲來控制生命周期,這里用suspend_always
做示例添加一個manual_lifetime
它是可平凡構造和可平凡析構的,但允許我們在需要時顯式構造 / 析構存儲的值。
template<typename T>
struct manual_lifetime {manual_lifetime() noexcept = default;~manual_lifetime() = default;// 不可復制/移動manual_lifetime(const manual_lifetime&) = delete;manual_lifetime(manual_lifetime&&) = delete;manual_lifetime& operator=(const manual_lifetime&) = delete;manual_lifetime& operator=(manual_lifetime&&) = delete;template<typename Factory>requiresstd::invocable<Factory&> &&std::same_as<std::invoke_result_t<Factory&>, T>T& construct_from(Factory factory) noexcept(std::is_nothrow_invocable_v<Factory&>) {return *::new (static_cast<void*>(&storage)) T(factory());}void destroy() noexcept(std::is_nothrow_destructible_v<T>) {std::destroy_at(std::launder(reinterpret_cast<T*>(&storage)));}T& get() & noexcept {return *std::launder(reinterpret_cast<T*>(&storage));}private:alignas(T) std::byte storage[sizeof(T)];
};
??基于此在__g_state
中添加對應的數據成員。
struct __g_state {__g_state(int&& x);int x;__g_promise_t __promise;manual_lifetime<std::suspend_always> __tmp1;// 待填充
};
??一旦我們通過調用intial_suspend()
構造了這個對象,我們就需要調用三個方法來實現co_await
表達式:await_ready()
、await_suspend()
和await_resume()
。調用await_suspend()
時,我們需要向它傳遞當前協程的句柄。目前,我們可以只調用std::coroutine_handle<__g_promise_t>::from_promise()
并傳遞對該promise_type
的引用。稍后我們會詳細了解其內部工作原理。
task g(int x) {std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));decltype(auto) return_value = state->__promise.get_return_object();state->__tmp1.construct_from([&]() -> decltype(auto) {return state->__promise.initial_suspend();});if (!state->__tmp1.get().await_ready()) {//// ... 在這里暫停協程//state->__tmp1.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));state.release();// 向下執行到下面的return語句} else {// 協程沒有暫停state.release();//// ... 開始執行協程體//}return return_value;
}
記錄暫停點
??當協程暫停時,它需要確保在恢復時能回到暫停時的控制流位置。它還需要跟蹤每個暫停點處哪些自動存儲期對象處于活動狀態,以便知道如果協程被銷毀(而不是恢復)時需要銷毀什么。實現這一點的一種方法是為協程中的每個暫停點分配一個唯一編號,并將其存儲在協程狀態的整數數據成員中。然后,每當協程暫停時,它會將暫停點的編號寫入協程狀態;當它被恢復 / 銷毀時,我們會檢查這個整數,看看它暫停在哪個暫停點。因此,我們擴展協程狀態,添加一個整數數據成員來存儲暫停點索引,并將其初始化為 0,只需要在適當的時機更新該暫停點的值即可:
struct __g_state {__g_state(int&& x);int x;__g_promise_t __promise;int __suspend_point = 0; // <-- 添加暫停點索引manual_lifetime<std::suspend_always> __tmp1;// 待填充
};
實現coroutine_handle::resume()和coroutine_handle::destroy()
??調用resume
和destroy
都會導致協程體的執行,只是resume
會在暫停點恢復執行,而destroy
會直接跳轉到協程體的結束。在實現 C++ 協程的coroutine_handle
類型時,我們需要通過類型擦除的方式存儲協程狀態的恢復和銷毀函數指針,以支持對任意協程實例的管理。這種設計使得 coroutine_handle
只包含一個指向協程狀態的指針,并通過狀態對象中的函數指針進行恢復和銷毀操作,同時提供方法在 void*
和具體狀態之間轉換。
??此外,為了確保函數指針的布局在所有協程狀態類型中保持一致,我們可以讓每個協程狀態類型繼承自一個包含這些數據成員的基類。這種方法使得協程能夠通過任何指向該協程的句柄進行恢復和銷毀,而不僅限于最近一次調用時傳遞的句柄。
struct __coroutine_state {using __resume_fn = void(__coroutine_state*);using __destroy_fn = void(__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;
};
??在協程handle
的resume
中只需要調用函數指針即可。
namespace std {template<typename Promise = void>class coroutine_handle;template<>class coroutine_handle<void> {public:coroutine_handle() noexcept = default;coroutine_handle(const coroutine_handle&) noexcept = default;coroutine_handle& operator=(const coroutine_handle&) noexcept = default;void* address() const {return static_cast<void*>(state_);}static coroutine_handle from_address(void* ptr) {coroutine_handle h;h.state_ = static_cast<__coroutine_state*>(ptr);return h;}explicit operator bool() noexcept {return state_ != nullptr;}friend bool operator==(coroutine_handle a, coroutine_handle b) noexcept {return a.state_ == b.state_;}void resume() const {state_->__resume(state_);}void destroy() const {state_->__destroy(state_);}bool done() const {return state_->__resume == nullptr;}private:__coroutine_state* state_ = nullptr;};
}
實現coroutine_handle::promise()和from_promise()
??對于更通用的coroutine_handle<Promise>
特化,大多數實現可以直接復用coroutine_handle<void>
的實現。然而,我們還需要能夠訪問協程狀態的promise_type
(通過promise()
方法返回),以及能從promise_type
的引用構造coroutine_handle
。因此,我們需要定義一個新的協程狀態基類,它繼承自__coroutine_state
并包含promise_type
,以便我們可以定義所有使用特定promise_type
的協程狀態類型都繼承自這個基類。同時,由于promise_type
的構造函數可能需要傳遞參數副本的引用,我們需要promise_type
的構造函數在參數副本的構造函數之后調用。因此我們在這個基類中為promise_type
預留存儲,使其相對于協程狀態的起始位置有一個固定的偏移量,但讓派生類負責在參數副本初始化后的適當位置調用構造函數 / 析構函數來實現類似的控制。
template<typename Promise>
struct __coroutine_state_with_promise : __coroutine_state {__coroutine_state_with_promise() noexcept {}~__coroutine_state_with_promise() {}union {Promise __promise;};
};
??然后更新__g_state
類,使其繼承自這個新基類:
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {__g_state(int&& __x): x(static_cast<int&&>(__x)) {// 使用 placement-new 在基類中初始化承諾對象::new ((void*)std::addressof(this->__promise))__g_promise_t(construct_promise<__g_promise_t>(x));}~__g_state() {// 還需要在參數對象銷毀前手動調用承諾析構函數this->__promise.~__g_promise_t();}int __suspend_point = 0;int x;manual_lifetime<std::suspend_always> __tmp1;// 待填充
};
??有了上面的基礎,就可以定義std::coroutine_handle<Promise>
類模板了:
namespace std {template<typename Promise>class coroutine_handle {using state_t = __coroutine_state_with_promise<Promise>;public:coroutine_handle() noexcept = default;coroutine_handle(const coroutine_handle&) noexcept = default;coroutine_handle& operator=(const coroutine_handle&) noexcept = default;operator coroutine_handle<void>() const noexcept {return coroutine_handle<void>::from_address(address());}explicit operator bool() const noexcept {return state_ != nullptr;}friend bool operator==(coroutine_handle a, coroutine_handle b) noexcept {return a.state_ == b.state_;}void* address() const {return static_cast<void*>(static_cast<__coroutine_state*>(state_));}static coroutine_handle from_address(void* ptr) {coroutine_handle h;h.state_ = static_cast<state_t*>(static_cast<__coroutine_state*>(ptr));return h;}Promise& promise() const {return state_->__promise;}static coroutine_handle from_promise(Promise& promise) {coroutine_handle h;// 我們知道__promise成員的地址,因此通過從該地址減去__promise字段的偏移量來計算協程狀態的地址h.state_ = reinterpret_cast<state_t*>(reinterpret_cast<unsigned char*>(std::addressof(promise)) -offsetof(state_t, __promise));return h;}// 用coroutine_handle<void>的實現來定義這些void resume() const {static_cast<coroutine_handle<void>>(*this).resume();}void destroy() const {static_cast<coroutine_handle<void>>(*this).destroy();}bool done() const {return static_cast<coroutine_handle<void>>(*this).done();}private:state_t* state_;};
}
協程體的開端
??先向前聲明正確簽名的恢復 / 銷毀函數,并更新__g_state
構造函數以初始化協程狀態,使恢復 / 銷毀函數指針指向它們:
void __g_resume(__coroutine_state* s);
void __g_destroy(__coroutine_state* s);struct __g_state : __coroutine_state_with_promise<__g_promise_t> {__g_state(int&& __x): x(static_cast<int&&>(__x)) {// 初始化coroutine_handle方法使用的函數指針this->__resume = &__g_resume;this->__destroy = &__g_destroy;// 使用placement-new在基類中初始化承諾對象::new ((void*)std::addressof(this->__promise))__g_promise_t(construct_promise<__g_promise_t>(x));}// ... 其余部分省略以簡潔起見
};task g(int x) {std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));decltype(auto) return_value = state->__promise.get_return_object();state->__tmp1.construct_from([&]() -> decltype(auto) {return state->__promise.initial_suspend();});if (!state->__tmp1.get().await_ready()) {state->__tmp1.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));state.release();// 向下執行到下面的return語句} else {// 協程沒有暫停。立即開始執行體__g_resume(state.release());}return return_value;
}
??resume/destroy
兩個函數差不多都是根據暫停點索引生成跳轉到代碼中正確位置的跳轉表,區別只是前者需要主動恢復協程,后者要銷毀對應的數據和狀態。
void __g_resume(__coroutine_state* s) {// 我們知道's'指向__g_stateauto* state = static_cast<__g_state*>(s);// 根據暫停點索引生成跳轉到代碼中正確位置的跳轉表switch (state->__suspend_point) {case 0: goto suspend_point_0;default: std::unreachable();}suspend_point_0:state->__tmp1.get().await_resume();state->__tmp1.destroy();// TODO: 實現協程體的其余部分//// int fx = co_await f(x);// co_return fx * fx;
}void __g_destroy(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);switch (state->__suspend_point) {case 0: goto suspend_point_0;default: std::unreachable();}suspend_point_0:state->__tmp1.destroy();goto destroy_state;// TODO: 為其他暫停點添加額外邏輯destroy_state:delete state;
}
co_await表達式
??對于co_await
首先需要求值,我們的場景中首先需要求值f(x)
,它返回一個臨時的task
對象。由于臨時task
直到語句末尾的分號才會被銷毀,且該語句包含co_await
表達式,因此task的生命周期跨越了暫停點,因此它必須存儲在協程狀態中。當對這個臨時task
求值co_await
表達式時,我們需要調用operator co_await()
方法,該方法返回一個臨時的awaiter
對象。這個對象的生命周期也跨越了暫停點,因此也必須存儲在協程狀態中。
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {__g_state(int&& __x);~__g_state();int __suspend_point = 0;int x;manual_lifetime<std::suspend_always> __tmp1;manual_lifetime<task> __tmp2;manual_lifetime<task::awaiter> __tmp3;
};
??既然添加了__tmp2
和__tmp3
,我們需要在__g_destroy
函數中添加對應的銷毀邏輯。同時,注意task::awaiter::await_suspend()
方法返回一個協程句柄,因此我們需要生成代碼來恢復返回的句柄。我們還需要在調用await_suspend()
之前更新暫停點索引(我們將為此暫停點使用索引 1),然后在跳轉表中添加一個額外的條目,確保我們能回到正確的位置恢復。
void __g_resume(__coroutine_state* s) {// 我們知道's'指向__g_stateauto* state = static_cast<__g_state*>(s);// 根據暫停點索引生成跳轉到代碼中正確位置的跳轉表switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // <-- 添加新的跳轉表條目default: std::unreachable();}suspend_point_0:state->__tmp1.get().await_resume();state->__tmp1.destroy();// int fx = co_await f(x);state->__tmp2.construct_from([&] {return f(state->x);});state->__tmp3.construct_from([&] {return static_cast<task&&>(state->__tmp2.get()).operator co_await();});if (!state->__tmp3.get().await_ready()) {// 標記暫停點state->__suspend_point = 1;auto h = state->__tmp3.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));// 在返回前恢復返回的協程句柄h.resume();return;}suspend_point_1:int fx = state->__tmp3.get().await_resume();state->__tmp3.destroy();state->__tmp2.destroy();// TODO: 實現// co_return fx * fx;
}void __g_destroy(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // <-- 添加新的跳轉表條目default: std::unreachable();}suspend_point_0:state->__tmp1.destroy();goto destroy_state;suspend_point_1:state->__tmp3.destroy();state->__tmp2.destroy();goto destroy_state;// TODO: 為其他暫停點添加額外邏輯destroy_state:delete state;
}
實現unhandled_exception()
??協程的行為就像其函數體被替換為:
{promise-type promise promise-constructor-arguments ;try {co_await promise.initial_suspend() ;function-body} catch ( ... ) {if (!initial-await-resume-called)throw ;promise.unhandled_exception() ;}final-suspend :co_await promise.final_suspend() ;
}
??我們已經在啟動函數中單獨處理了initial-await_resume-called
分支,需要處理resume/destroy
拋出的異常。如果從返回的協程的.resume()
調用中拋出異常,它不應被當前協程捕獲,而應傳播出恢復此協程的resume()
調用。因此,我們將協程句柄存儲在函數頂部聲明的變量中,然后goto
到 try/catch
之外的點,并在那里執行.resume()
調用。
void __g_resume(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);std::coroutine_handle<void> coro_to_resume;try {switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // <-- 添加新的跳轉表條目default: std::unreachable();}suspend_point_0:state->__tmp1.get().await_resume();state->__tmp1.destroy();// int fx = co_await f(x);state->__tmp2.construct_from([&] {return f(state->x);});state->__tmp3.construct_from([&] {return static_cast<task&&>(state->__tmp2.get()).operator co_await();});if (!state->__tmp3.get().await_ready()) {state->__suspend_point = 1;coro_to_resume = state->__tmp3.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));goto resume_coro;}suspend_point_1:int fx = state->__tmp3.get().await_resume();state->__tmp3.destroy();state->__tmp2.destroy();// TODO: 實現// co_return fx * fx;} catch (...) {state->__promise.unhandled_exception();goto final_suspend;}final_suspend:// TODO: 實現// co_await promise.final_suspend();resume_coro:coro_to_resume.resume();return;
}
??然而,上面的代碼存在一個錯誤。如果__tmp3.get().await_resume()
調用拋出異常,我們將無法在捕獲異常之前調用__tmp3
和__tmp2
的析構函數。注意,我們不能簡單地捕獲異常、調用析構函數然后重新拋出異常,因為這會改變那些析構函數的行為 —— 如果它們調用std::unhandled_exceptions()
,由于異常已被 “處理”,返回值會不同。然而,如果析構函數在異常展開期間調用它,std::unhandled_exceptions()
的調用應該返回非零值。相反,我們可以定義一個 RAII 輔助類,確保在拋出異常時在作用域退出時調用析構函數。
template<typename T>
struct destructor_guard {explicit destructor_guard(manual_lifetime<T>& obj) noexcept: ptr_(std::addressof(obj)){}// 不可移動destructor_guard(destructor_guard&&) = delete;destructor_guard& operator=(destructor_guard&&) = delete;~destructor_guard() noexcept(std::is_nothrow_destructible_v<T>) {if (ptr_ != nullptr) {ptr_->destroy();}}void cancel() noexcept { ptr_ = nullptr; }private:manual_lifetime<T>* ptr_;
};// 對不需要調用析構函數的類型的部分特化
template<typename T>requires std::is_trivially_destructible_v<T>
struct destructor_guard<T> {explicit destructor_guard(manual_lifetime<T>&) noexcept {}void cancel() noexcept {}
};// 類模板參數推導以簡化使用
template<typename T>
destructor_guard(manual_lifetime<T>& obj) -> destructor_guard<T>;void __g_resume(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);std::coroutine_handle<void> coro_to_resume;try {switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // <-- 添加新的跳轉表條目default: std::unreachable();}suspend_point_0:{destructor_guard tmp1_dtor{state->__tmp1};state->__tmp1.get().await_resume();}// int fx = co_await f(x);{state->__tmp2.construct_from([&] {return f(state->x);});destructor_guard tmp2_dtor{state->__tmp2};state->__tmp3.construct_from([&] {return static_cast<task&&>(state->__tmp2.get()).operator co_await();});destructor_guard tmp3_dtor{state->__tmp3};if (!state->__tmp3.get().await_ready()) {state->__suspend_point = 1;coro_to_resume = state->__tmp3.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));// 協程暫停時不退出作用域// 因此取消析構保護tmp3_dtor.cancel();tmp2_dtor.cancel();goto resume_coro;}// 不要在這里退出作用域//// 我們不能'goto'到進入具有非平凡析構函數的變量作用域的標簽// 因此我們必須在不調用析構函數的情況下退出析構保護的作用域,然后在`suspend_point_1`標簽后重新創建它們tmp3_dtor.cancel();tmp2_dtor.cancel();}suspend_point_1:int fx = [&]() -> decltype(auto) {destructor_guard tmp2_dtor{state->__tmp2};destructor_guard tmp3_dtor{state->__tmp3};return state->__tmp3.get().await_resume();}();// TODO: 實現// co_return fx * fx;} catch (...) {state->__promise.unhandled_exception();goto final_suspend;}final_suspend:// TODO: 實現// co_await promise.final_suspend();resume_coro:coro_to_resume.resume();return;
}
??對于promise.unhandled_exception()
方法本身拋出異常的情況(例如,如果它重新拋出當前異常),可能需要特殊處理。這種情況下,協程需要捕獲異常,將協程標記為在最終暫停點暫停,然后重新拋出異常。
__g_resume(){//省略部分代碼............try {// ...} catch (...) {try {state->__promise.unhandled_exception();} catch (...) {state->__suspend_point = 2;state->__resume = nullptr; // 標記為最終暫停點throw;}}//省略部分代碼............
}__g_destroy(){//省略部分代碼............switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1;case 2: goto destroy_state; // 沒有需要銷毀的作用域內變量// 只需銷毀協程狀態對象} //省略部分代碼............
}
實現co_return
??co_return <expr>
實現相對簡單:
state->__promise.return_value(fx * fx);
goto final_suspend;
實現final_suspend()
??final_suspend()
方法返回一個臨時的task::promise_type::final_awaiter
類型,需要將其存儲在協程狀態中,并在__g_destroy
中銷毀。這種類型沒有自己的operator co_await()
,因此我們不需要為該調用的結果準備額外的臨時對象。與task::awaiter
類型一樣,它也使用返回協程句柄的await_suspend()
形式。因此,我們需要確保對返回的句柄調用resume()
。如果協程不在最終暫停點暫停,則協程狀態會被隱式銷毀。因此,如果執行到達協程末尾,我們需要刪除狀態對象。此外,由于所有最終暫停邏輯都要求是 noexcept
的,不需要擔心任何子表達式會拋出異常。
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {__g_state(int&& __x);~__g_state();int __suspend_point = 0;int x;manual_lifetime<std::suspend_always> __tmp1;manual_lifetime<task> __tmp2;manual_lifetime<task::awaiter> __tmp3;manual_lifetime<task::promise_type::final_awaiter> __tmp4; // <---
};
??final_suspend()
的實現:
final_suspend:// co_await promise.final_suspend{state->__tmp4.construct_from([&]() noexcept {return state->__promise.final_suspend();});destructor_guard tmp4_dtor{state->__tmp4};if (!state->__tmp4.get().await_ready()) {state->__suspend_point = 2;state->__resume = nullptr; // 標記為最終暫停點coro_to_resume = state->__tmp4.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));tmp4_dtor.cancel();goto resume_coro;}state->__tmp4.get().await_resume();}// 如果執行流到達協程末尾,則銷毀協程狀態delete state;return;
??最終,還需要更新__g_destroy
函數來處理這個新的暫停點:
void __g_destroy(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1;case 2: goto suspend_point_2;default: std::unreachable();}suspend_point_0:state->__tmp1.destroy();goto destroy_state;suspend_point_1:state->__tmp3.destroy();state->__tmp2.destroy();goto destroy_state;suspend_point_2:state->__tmp4.destroy();goto destroy_state;destroy_state:delete state;
}
實現對稱轉移和空操作協程
??協程規范中強烈建議編譯器以尾調用的方式實現下一個協程的恢復,而不是遞歸地恢復下一個協程。這是因為如果協程在循環中相互恢復,遞歸地恢復下一個協程很容易導致無界的棧增長。而上面實現的__g_resume()
函數體內調用下一個協程的.resume()
,然后返回,因此__g_resume()幀使用的棧空間要到下一個協程暫停并返回后才會釋放。
??編譯器能夠通過將下一個協程的恢復實現為尾調用來做到這一點。通過這種方式,編譯器生成的代碼會先彈出當前棧幀(保留返回地址),然后執行jmp到下一個協程的恢復函數。由于在 C++ 中沒有機制指定尾位置的函數調用應該是尾調用,我們需要從恢復函數返回,以便釋放其棧空間,然后讓調用者恢復下一個協程。由于下一個協程在暫停時可能還需要恢復另一個協程,而且這可能會無限進行下去,調用者需要在循環中恢復協程。這種循環通常稱為 “蹦床循環”(trampoline loop),因為我們從一個協程返回到循環,然后從循環 “反彈” 到下一個協程。如果我們將恢復函數的簽名修改為返回下一個協程的協程狀態指針(而不是返回 void),那么coroutine_handle::resume()
函數可以立即調用下一個協程的__resume()
函數指針來恢復它。
??因此修改__coroutine_state
的__resume_fn
簽名:
struct __coroutine_state {using __resume_fn = __coroutine_state* (__coroutine_state*);using __destroy_fn = void (__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;
};
??可以這樣編寫coroutine_handle::resume()
函數:
void std::coroutine_handle<void>::resume() const {__coroutine_state* s = state_;do {s = s->__resume(s);} while (/* 某種條件 */);
}
??現在的問題是如何添加終止條件。std::noop_coroutine()
是一個工廠函數,返回一個特殊的協程句柄,它具有空操作(no-op)的 resume()
和 destroy()
方法。如果一個協程暫停并從 await_suspend()
方法返回空操作協程句柄,這表明沒有更多的協程需要恢復,恢復此協程的 coroutine_handle::resume()
調用應該返回到其調用者。因此,我們需要實現 std::noop_coroutine()
和 coroutine_handle::resume()
中的條件,以便當 __coroutine_state
指針指向空操作協程狀態時,條件返回 false
,循環退出。我們可以使用的一種策略是定義一個 __coroutine_state
的靜態實例,指定為空操作協程狀態。std::noop_coroutine()
函數可以返回一個指向此對象的協程句柄,我們可以將 __coroutine_state
指針與該對象的地址進行比較,以查看特定的協程句柄是否是空操作協程。
struct __coroutine_state {using __resume_fn = __coroutine_state* (__coroutine_state*);using __destroy_fn = void (__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;static __coroutine_state* __noop_resume(__coroutine_state* state) noexcept {return state;}static void __noop_destroy(__coroutine_state*) noexcept {}static const __coroutine_state __noop_coroutine;
};inline const __coroutine_state __coroutine_state::__noop_coroutine{&__coroutine_state::__noop_resume,&__coroutine_state::__noop_destroy
};namespace std {struct noop_coroutine_promise {};using noop_coroutine_handle = coroutine_handle<noop_coroutine_promise>;noop_coroutine_handle noop_coroutine() noexcept;template<>class coroutine_handle<noop_coroutine_promise> {public:constexpr coroutine_handle(const coroutine_handle&) noexcept = default;constexpr coroutine_handle& operator=(const coroutine_handle&) noexcept = default;constexpr explicit operator bool() noexcept { return true; }constexpr friend bool operator==(coroutine_handle, coroutine_handle) noexcept {return true;}operator coroutine_handle<void>() const noexcept {return coroutine_handle<void>::from_address(address());}noop_coroutine_promise& promise() const noexcept {static noop_coroutine_promise promise;return promise;}constexpr void resume() const noexcept {}constexpr void destroy() const noexcept {}constexpr bool done() const noexcept { return false; }constexpr void* address() const noexcept {return const_cast<__coroutine_state*>(&__coroutine_state::__noop_coroutine);}private:constexpr coroutine_handle() noexcept = default;friend noop_coroutine_handle noop_coroutine() noexcept {return {};}};
}void std::coroutine_handle<void>::resume() const {__coroutine_state* s = state_;do {s = s->__resume(s);} while (s != &__coroutine_state::__noop_coroutine);
}__coroutine_state* __g_resume(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);try {switch (state->__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // <-- 添加新的跳轉表條目default: std::unreachable();}suspend_point_0:{destructor_guard tmp1_dtor{state->__tmp1};state->__tmp1.get().await_resume();}// int fx = co_await f(x);{state->__s1.__tmp2.construct_from([&] {return f(state->x);});destructor_guard tmp2_dtor{state->__s1.__tmp2};state->__s1.__tmp3.construct_from([&] {return static_cast<task&&>(state->__s1.__tmp2.get()).operator co_await();});destructor_guard tmp3_dtor{state->__s1.__tmp3};if (!state->__s1.__tmp3.get().await_ready()) {state->__suspend_point = 1;auto h = state->__s1.__tmp3.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));// 協程暫停時不退出作用域// 因此取消析構保護tmp3_dtor.cancel();tmp2_dtor.cancel();return static_cast<__coroutine_state*>(h.address());}// 不要在這里退出作用域// 我們不能'goto'到進入具有非平凡析構函數的變量作用域的標簽// 因此我們必須在不調用析構函數的情況下退出析構保護的作用域,然后在`suspend_point_1`標簽后重新創建它們tmp3_dtor.cancel();tmp2_dtor.cancel();}suspend_point_1:int fx = [&]() -> decltype(auto) {destructor_guard tmp2_dtor{state->__s1.__tmp2};destructor_guard tmp3_dtor{state->__s1.__tmp3};return state->__s1.__tmp3.get().await_resume();}();// co_return fx * fx;state->__promise.return_value(fx * fx);goto final_suspend;} catch (...) {state->__promise.unhandled_exception();goto final_suspend;}final_suspend:// co_await promise.final_suspend{state->__tmp4.construct_from([&]() noexcept {return state->__promise.final_suspend();});destructor_guard tmp4_dtor{state->__tmp4};if (!state->__tmp4.get().await_ready()) {state->__suspend_point = 2;state->__resume = nullptr; // 標記為最終暫停點auto h = state->__tmp4.get().await_suspend(std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));tmp4_dtor.cancel();return static_cast<__coroutine_state*>(h.address());}state->__tmp4.get().await_resume();}// 如果執行流到達協程末尾,則銷毀協程狀態delete state;return static_cast<__coroutine_state*>(std::noop_coroutine().address());
}
協程state的內存占用優化
??協程狀態類型__g_state
實際上比需要的更大。然而,一些臨時值的生命周期不重疊,因此理論上我們可以通過在一個對象的生命周期結束后重用其存儲來節省協程狀態的空間。由于__tmp2
和__tmp3
的生命周期重疊,我們必須將它們一起放在一個結構體中,因為它們都需要同時存在。然而,__tmp1和__tmp4的生命周期不重疊,因此它們可以一起放在匿名union
中。
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {__g_state(int&& x);~__g_state();int __suspend_point = 0;int x;struct __scope1 {manual_lifetime<task> __tmp2;manual_lifetime<task::awaiter> __tmp3;};union {manual_lifetime<std::suspend_always> __tmp1;__scope1 __s1;manual_lifetime<task::promise_type::final_awaiter> __tmp4;};
};
3 協程使用可能存在問題
3.1 避免內存分配
??異步操作通常需要存儲一些每個操作的狀態,以跟蹤操作的進展。這種狀態通常需要在操作持續期間保持有效,并且只有在操作完成后才能釋放。例如,調用異步 Win32 I/O 函數時,需要分配并傳遞一個指向 OVERLAPPED 結構的指針。調用者負責確保該指針在操作完成前保持有效。
??在傳統的基于回調的 API 中,這種狀態通常需要在堆上分配,以確保它具有適當的生命周期。如果您執行多個操作,可能需要為每個操作分配和釋放這種狀態。如果性能是一個問題,可以使用自定義分配器,從池中分配這些狀態對象。然而,當我們使用協程時,可以避免為操作狀態進行堆分配,因為協程幀中的局部變量在協程掛起時會保持有效。通過將每個操作的狀態放在 Awaiter 對象中,我們可以有效地“借用”協程幀的內存,用于存儲每個操作的狀態,直到 co_await 表達式完成。一旦操作完成,協程恢復,Awaiter 對象被銷毀,從而釋放協程幀中的內存供其他局部變量使用。
??最終,協程幀可能仍然在堆上分配。然而,一旦分配,協程幀可以用于執行多個異步操作,而只需那一次堆分配。如果仔細考慮,協程幀實際上充當了一種高性能的區域內存分配器。編譯器在編譯時確定所需的總區域大小,然后能夠以零開銷的方式將這塊內存分配給局部變量。
3.2 理清協程和線程的區別
??協程和線程都是用來實現異步編程的手段而已,都是在不同維度上所對應的產物。很多文章會將進程,線程,協程放在一起做描述區分,我個人理解其實不需要這么復雜,直接從執行層次上區分即可。對于用戶態程序來講,其執行代碼從上到下的層次分別為協程/函數,系統線程,邏輯線程(或者叫硬件線程,這里不做區分)。任何用戶態的代碼最終要運行到CPU上都是要運行到硬件線程單元上的,只不過為了方便開發,將其線程模型通過操作系統包裝成了系統線程(一般是m-n模型)。系統線程由操作系統調度,但是最終都會對應到有限的硬件線程上。協程類似,協程的異步是將異步調度權放在了用戶態,可以認為是在線程上的更上一層包裝,讓用戶態可以調度自己的任務。而且按照這個層次,以用戶態的視角觀察,越往上切換的開銷約小,性能越優化,開發的靈活性越大。因此,C++ 標準提供的只是最基本的協程支持,如果要更合適的調度可以根據自己的開發場景開發對應的協程調度庫來方便開發。
??當然協程和線程關系又不是那么簡單,雖然最終協程的代碼運行都會落到線程上,但是協程的運行規則相比線程要復雜的多,需要相比線程更好的調度規劃才能達到更好的性能。同時協程可以在一個線程上執行,也可以在多個線程上執行,這完全取決于開發者的意愿。所以在開發時,如果協程的切換存在線程切換也是要考慮多線程問題的。
??另外,根據現有的開發狀態來講,C++ 協程和線程是不同生態位的東西,是相互彌補的。協程的編寫和管理相對簡單,尤其在處理非阻塞 I/O 時,可以讓代碼更清晰,避免回調地獄。協程在用戶態中進行調度,具有更輕量級的特性,適合處理大量的異步操作,如 I/O 密集型任務。它們能有效減少上下文切換的開銷,提高程序的響應性。線程能夠利用操作系統的調度能力,更好地處理需要并行計算的復雜任務。線程則在多核處理器上更有效,適合 CPU 密集型任務。線程可以并行執行,充分利用多核 CPU 的計算能力。
3.3 對稱轉移
??對稱轉移是 C++20 協程中新增的關鍵功能,允許一個協程暫停時直接將執行權轉移給另一個暫停的協程,且不產生額外棧空間消耗。其核心是通過await_suspend()返回std::coroutine_handle
實現協程間的 “對稱” 切換,配合編譯器的尾調用優化(確保棧幀不累積),避免傳統遞歸調用導致的棧溢出。同時,通過std::noop_coroutine()
可在無其他協程可恢復時,將執行權返回給resume()的調用者。
??在傳統協程實現中,若協程通過co_await嵌套調用(如循環中同步完成的任務),會因每次resume()調用在棧上累積幀,導致類似遞歸的棧溢出。考慮下面的例子:
// 不支持對稱轉移的task類型實現(會導致棧溢出)
class task {
public:class promise_type {public:task get_return_object() noexcept {return task{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }void return_void() noexcept {}void unhandled_exception() noexcept { std::terminate(); }struct final_awaiter {bool await_ready() noexcept { return false; }// 直接resume導致棧幀累積void await_suspend(std::coroutine_handle<promise_type> h) noexcept {h.promise().continuation.resume(); }void await_resume() noexcept {}};final_awaiter final_suspend() noexcept { return {}; }std::coroutine_handle<> continuation;};task(task&& t) noexcept : coro_(std::exchange(t.coro_, {})) {}task(const task&) = delete;task& operator=(const task&) = delete;~task() {if (coro_) coro_.destroy();}class awaiter {public:bool await_ready() noexcept { return false; }// 直接resume被等待協程,棧幀疊加void await_suspend(std::coroutine_handle<> continuation) noexcept {coro_.promise().continuation = continuation;coro_.resume(); // 每次調用都會新增棧幀}void await_resume() noexcept {}private:friend task;explicit awaiter(std::coroutine_handle<promise_type> h) noexcept : coro_(h) {}std::coroutine_handle<promise_type> coro_;};awaiter operator co_await() && noexcept {return awaiter{coro_};}// 新增:啟動協程執行(關鍵修改,確保協程實際運行)void start() noexcept {if (coro_) coro_.resume();}private:explicit task(std::coroutine_handle<promise_type> h) noexcept : coro_(h) {}std::coroutine_handle<promise_type> coro_;
};// 同步完成的協程
task completes_synchronously() {co_return; // 立即完成,觸發final_suspend
}// 循環等待同步任務(棧溢出的根源)
task loop_synchronously(int count) {for (int i = 0; i < count; ++i) {co_await completes_synchronously(); // 每次循環都會嵌套resume}
}// 啟動器協程:用于觸發loop_synchronously執行
task start_loop(int count) {co_await loop_synchronously(count); // 啟動循環協程
}int testSym() {spdlog::set_level(spdlog::level::info);spdlog::info("Starting test (will crash due to stack overflow)");// 關鍵修改:創建啟動器并執行,觸發完整調用鏈auto t = start_loop(1'000'000);t.start(); // 啟動根協程,開始執行整個調用鏈spdlog::info("This line will never be reached");return 0;
}
??上述代碼中,loop_synchronously
協程在循環中反復co_await completes_synchronously
,形成了一種 “隱性遞歸” 的執行模式。我們通過拆解單次循環的執行步驟,分析棧幀的變化:
- 啟動根協程,初始化棧幀:
testSym
函數中,start_loop(1'000'000)
創建一個task
對象,隨后調用t.start()
觸發根協程start_loop
執行。start_loop
的resume()
被調用,棧上創建第一個棧幀:start_loop$resume
(協程體執行部分)。
start_loop
等待loop_synchronously
,棧幀 + 1start_loop
執行co_await loop_synchronously(1'000'000)
,觸發loop_synchronously
的創建:loop_synchronously
的協程幀在堆上分配,初始掛起后返回task
對象。start_loop
暫停,調用loop_synchronously
的await_suspend
方法,該方法通過coro_.resume()
恢復loop_synchronously
執行。此時棧上新增第二個棧幀:loop_synchronously$resume
。
loop_synchronously
第一次循環,等待completes_synchronously
,棧幀 + 2loop_synchronously
進入循環,執行co_await completes_synchronously()
:completes_synchronously
創建并掛起,返回task
對象。
loop_synchronously
暫停,調用completes_synchronously
的await_suspend
方法,該方法通過coro_.resume()
恢復completes_synchronously
執行。- 棧上新增第三個棧幀:
completes_synchronously$resume
。completes_synchronously
執行到co_return
,觸發final_suspend
,其final_awaiter
的await_suspend
調用loop_synchronously
的resume()
(恢復循環)。 - 棧上新增第四個棧幀:
loop_synchronously$resume
(第二次進入循環體)。
- 循環累積,棧幀無界增長
- 每次循環迭代中,
loop_synchronously
和completes_synchronously
會相互通過resume()
恢復對方執行: completes_synchronously
完成后,final_awaiter
調用loop_synchronously.resume()
,棧上新增loop_synchronously$resume
幀。loop_synchronously
再次co_await
時,調用completes_synchronously.resume()
,棧上新增completes_synchronously$resume
幀。
- 每次循環迭代中,
??每輪循環會新增2 個棧幀,且這些幀在循環結束前不會被釋放(因為resume()
的調用者仍在棧上等待返回),當循環調用比較多時導致棧幀積累過多導致棧溢出。解決該問題的核心解決方案是避免協程切換時的棧幀累積。以下兩個方案:
- 通過
await_suspend()
返回std::coroutine_handle
實現協程間的 “對稱轉移”,配合編譯器的尾調用優化,在切換協程時釋放當前棧幀,避免累積。
class task {
public:class promise_type {public:// ...(其他代碼同前)struct final_awaiter {bool await_ready() noexcept { return false; }// 對稱轉移:返回延續協程的句柄(尾調用優化)std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept {return h.promise().continuation; // 直接返回句柄,不調用resume()}void await_resume() noexcept {}};// ...(其他代碼同前)};class awaiter {public:// ...(其他代碼同前)// 對稱轉移:返回被等待協程的句柄(尾調用優化)std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation) noexcept {coro_.promise().continuation = continuation;return coro_; // 返回句柄,由編譯器處理尾調用跳轉}};// ...(其他代碼同前)
};
- 使用原子變量檢測同步完成。通過
std::atomic
標記協程是否同步完成,若已完成則直接恢復當前協程。
class task {
public:class promise_type {public:// ...(其他代碼同前)std::atomic<bool> is_completed{false}; // 標記是否已完成std::coroutine_handle<> continuation;};class awaiter {public:// ...(其他代碼同前)// 返回bool:false表示直接恢復當前協程bool await_suspend(std::coroutine_handle<> continuation) noexcept {coro_.promise().continuation = continuation;coro_.resume(); // 執行被等待協程// 若已同步完成,返回false直接恢復當前協程(無棧幀累積)return !coro_.promise().is_completed.exchange(true);}};// 修改final_awaiter:同步完成時不調用resume()struct final_awaiter {bool await_ready() noexcept { return false; }void await_suspend(std::coroutine_handle<promise_type> h) noexcept {h.promise().is_completed = true; // 標記完成// 僅在異步完成時才恢復(同步完成時由await_suspend直接恢復)if (!h.promise().continuation.done()) {h.promise().continuation.resume();}}};
};
4 參考文獻
- Cppreference coroutine
- Coroutine Theory
- C++ Coroutines: Understanding operator co_await
- C++ Coroutines: Understanding the promise type
- C++ Coroutines: Understanding Symmetric Transfer
- C++ Coroutines: Understanding the Compiler Transform