協程是什么?
協程就是可以“暫停”和“繼續”的函數,像在函數里打個斷點,然后以后可以從斷點繼續運行,而不是重新開始。
線程 vs 協程:類比
想象你在寫小說:
線程:
你開了 3 個作者(線程),每個人都在寫書。
系統(OS)來回切換誰在寫。
切換的時候要收拾桌子、換椅子、拿走稿紙(上下文切換),開銷大。
協程:
你只有 1 個作者,但可以寫一會兒暫停,去做別的,再回來接著寫。
暫停的時候,只需在稿紙上插個書簽(掛起點),下次從書簽繼續,開銷極小。
特性 | 線程 | 協程 |
---|---|---|
切換方式 | 搶占式(OS決定) | 協作式(程序決定) |
切換成本 | 高(要保存寄存器、堆棧) | 低(只是保存協程狀態) |
并發 | 多線程可以真正并行 | 協程必須在同一個線程輪流跑 |
控制權 | OS 控制 | 程序員控制(通過 co_await ) |
協程 ≠ 線程,它只是單線程內的“任務切換”機制。
協程可以跑在一個線程上,也可以結合線程池,讓多個線程各自跑多個協程 → 混合使用。
協程不提供并行(不會用多個 CPU 核心),但可以輕松實現異步(不會阻塞線程)。
協程解決了什么問題?
傳統異步寫法(回調):
async_read(socket, buf, [](auto ec, auto size) {async_write(socket, buf, [](auto ec, auto size) {// 回調地獄});
});
協程寫法:
co_await async_read(socket, buf);
co_await async_write(socket, buf);
為什么開銷小?
線程切換:需要保存 CPU 寄存器、切換內核態、切換棧,可能上百納秒甚至微秒級。
協程切換:只保存協程狀態(程序計數器 + 局部變量),在用戶態完成,納秒級。
使用
C++20 正式引入協程機制,底層通過 編譯器轉換 + 棧幀對象(promise) 實現。
三個關鍵字:
co_await
—— 掛起并等待某個任務。co_yield
—— 掛起并返回一個值(類似生成器)。co_return
—— 返回最終值并結束協程。
協程的運行方式
協程不是線程,也不是普通函數,而是 編譯器把協程函數轉換成狀態機,并返回一個“協程句柄”。
流程:
編譯器檢測函數是否包含
co_await/co_yield/co_return
。如果包含,編譯器會生成:
協程狀態對象(包含局部變量、掛起點、promise)。
協程句柄(
std::coroutine_handle<>
)控制 resume/destroy。
第一次調用協程函數:
返回一個
awaitable
對象,不會立刻執行全部邏輯。
調用
resume()
:協程運行到
co_await
掛起。
再次
resume()
:從上次掛起點繼續。
關鍵字 | 作用 |
---|---|
co_return | 返回最終結果并結束協程 |
co_yield | 產出一個值并掛起(生成器) |
co_await | 掛起并等待某個操作完成 |
常見協程返回類型
std::generator<T>
(C++23 提供)自定義
task<T>
第三方庫:
cppcoro(最常用)
Boost.Coroutine
?協程常見寫法
(1)最簡單的生成器(C++23)
#include <coroutine>
#include <iostream>
#include <generator>std::generator<int> numbers() {for (int i = 0; i < 5; ++i)co_yield i;
}int main() {for (auto v : numbers())std::cout << v << "\n";
}
(2)異步任務(自定義 Task)
#include <coroutine>
#include <iostream>// 1. 定義返回類型 Task
struct Task {// 2. promise_type:編譯器需要它來生成協程對象struct promise_type {// 編譯器調用:創建協程返回值Task get_return_object() {return Task{std::coroutine_handle<promise_type>::from_promise(*this)};}// 協程開始前是否掛起?std::suspend_never initial_suspend() noexcept { return {}; }// 協程結束后是否掛起?std::suspend_never final_suspend() noexcept { return {}; }// 處理 co_returnvoid return_void() {}void unhandled_exception() {} // 異常處理};std::coroutine_handle<promise_type> handle;Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() { if(handle) handle.destroy(); }
};// 3. 協程函數:編譯器自動用 Task::promise_type 來管理
Task myCoroutine() {std::cout << "Hello\n";co_await std::suspend_always{}; // 掛起協程std::cout << "World\n";
}int main() {auto t = myCoroutine(); // 創建協程,執行到 co_await 掛起std::cout << "Resume...\n";t.handle.resume(); // 恢復協程,繼續執行
}
自定義 Task詳細解析
為什么要定義 promise_type
C++20 協程和普通函數完全不同,編譯器在看到協程時不會直接返回值,而是生成一個狀態機對象(Frame)。
協程的返回類型必須定義一個 嵌套類型
promise_type
。編譯器規則:
當你寫:
Task myCoroutine() {co_await something;co_return;
}
編譯器會查找 Task::promise_type
,并生成代碼去:
創建一個
promise_type
對象。調用它的
get_return_object()
,把結果作為協程的返回值。調用
initial_suspend()
/final_suspend()
控制是否立即掛起。調用
return_void()
或return_value()
處理co_return
。
所以:
promise_type
就是協程的“大腦”,控制協程生命周期。它必須提供一組固定接口(編譯器調用)。
initial_suspend和final_suspend? ? ? ?
initial_suspend()
:協程剛創建時是否掛起?返回
std::suspend_always
→ 掛起,調用者必須手動resume()
。返回
std::suspend_never
→ 不掛起,協程立即執行。
final_suspend()
:協程結束后是否掛起?一般返回
std::suspend_always
,讓調用者決定何時銷毀協程。
為什么要控制掛起?
這關系到執行模型(立即運行 or 手動恢復)。
std::coroutine_handle是干啥的
它是一個 輕量級句柄,指向協程幀(frame)。
協程幀包含:
狀態機(狀態編號)
局部變量
一個
promise_type
對象(由返回類型的promise_type
定義)
std::coroutine_handle
提供操作協程生命周期的方法:恢復協程(
resume()
)銷毀協程(
destroy()
)判斷是否執行完(
done()
)
本質:它類似一個 void*
,但知道協程幀布局,可以安全操作 promise 對象。
重點:協程不是線程,編譯器不會自動跑完,你必須顯式調用?resume()
,所以要有句柄。
為什么模板參數是 promise_type
?
協程幀里有一個
promise_type
成員。coroutine_handle<promise_type>
是一個類型安全的 handle,可以直接訪問promise
對象:
handle.promise() // 返回對 promise 對象的引用
?這就是為什么你的 Task
返回類型必須定義 promise_type
,編譯器才能生成 coroutine_handle<promise_type>
。
如果不關心 promise
,可以用 無模板版本:
std::coroutine_handle<> // 泛型 handle,不提供 promise()。
類成員函數?
1. resume()
恢復協程執行。
如果協程在掛起狀態,繼續執行,直到下一個掛起點或結束。
所以resume()的同步地、阻塞的
2. done()
判斷協程是否執行完畢(到達
final_suspend()
之后)。done()
== true 表示協程執行到final_suspend()
。
3. destroy()
銷毀協程幀(釋放內存),必須在協程完全結束后調用,否則 UB。
4. promise()
返回
promise_type&
,可以訪問自定義邏輯,比如取值、狀態。
舉例按流程詳細解釋
代碼流程:
#include <coroutine>
#include <iostream>// 1. 協程返回類型 Task
struct Task {struct promise_type {// 編譯器會調用此函數獲取協程返回對象Task get_return_object() {std::cout << "[promise_type] get_return_object()\n";return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept {std::cout << "[promise_type] initial_suspend()\n";return {}; // 創建后立即掛起}std::suspend_always final_suspend() noexcept {std::cout << "[promise_type] final_suspend()\n";return {}; // 協程結束后掛起,等外部銷毀}void return_void() { std::cout << "[promise_type] return_void()\n"; }void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;explicit Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() {if (handle) {std::cout << "[Task] destroy coroutine\n";handle.destroy(); // 銷毀協程 frame}}void resume() {std::cout << "[Task] resume()\n";handle.resume(); // 恢復協程}
};// 2. 協程函數:執行順序受 suspend 控制
Task myCoroutine() {std::cout << "[Coroutine] Start\n";co_await std::suspend_always{}; // 掛起std::cout << "[Coroutine] After co_await\n";co_return;
}int main() {auto t = myCoroutine(); // 創建協程(不會立刻跑完整個函數)std::cout << "[main] first resume\n";t.resume(); // 執行到 co_await 掛起std::cout << "[main] second resume\n";t.resume(); // 執行到 co_return,final_suspend 掛起
}
輸出:
[promise_type] get_return_object()
[promise_type] initial_suspend()
[main] first resume
[Task] resume()
[Coroutine] Start
[Coroutine] After co_await
[promise_type] return_void()
[promise_type] final_suspend()
[main] second resume
[Task] destroy coroutine
執行過程詳細解讀:
Step 1: 調用 myCoroutine()
編譯器看到
co_await
→ 把myCoroutine
改造成協程,不直接返回Task
,而是:編譯器硬編碼規則:
如果協程返回類型是
R
,就去找R::promise_type
。通過
R::promise_type
生成一個promise_type
?類的對象promise。
調用
get_return_object()
→ 返回Task
,并綁定coroutine_handle
。std::coroutine_handle<promise_type>::from_promise(*this)
:把 promise 和 handle 關聯。
調用
initial_suspend()
→ 返回suspend_always
,協程掛起,不執行函數體。
此時:協程 frame(狀態機)在堆上創建,但沒開始執行。
對象狀態:
Task
:持有coroutine_handle
。promise_type
:存儲協程狀態。handle
:指向協程 frame,能控制resume()
。
Step 2: t.resume()
調用
handle.resume()
。協程開始執行,打印
[Coroutine] Start
。遇到
co_await std::suspend_always{}
→ 立即掛起。編譯器調用
await_suspend()
(內部由suspend_always
實現)。
控制權返回
main
,協程停在co_await
這一行。
Step 3: 第二次 t.resume()
協程從
co_await
后恢復。打印
[Coroutine] After co_await
。執行
co_return
→ 編譯器調用promise_type.return_void()
。協程執行到尾,進入
final_suspend()
→ 返回suspend_always
,協程再次掛起,等待銷毀。
Step 4: 退出 main()
,Task
析構
調用
handle.destroy()
→ 銷毀協程 frame 和promise_type
。協程徹底結束。
關鍵點匯總
promise_type
是協程的“控制器”,編譯器強制要求它有:get_return_object()
(返回 Task)initial_suspend()
/final_suspend()
return_void()
/return_value()
coroutine_handle
是“遙控器”,用來resume()
和destroy()
。co_await
觸發掛起點。co_return
結束協程。狀態機 + Frame 在堆上管理生命周期。
co_yield使用詳細解釋
編譯器在看到 co_yield expr;
時,會將它轉換成 對 promise_type.yield_value(expr)
的調用,并把返回值當作 co_await
的對象。
也就是說:
co_yield value;
等價于:
co_await promise.yield_value(value);
所以:
co_yield
會調用promise_type::yield_value(T value)
。這個函數必須返回一個 Awaitable 對象(通常是
std::suspend_always
或std::suspend_never
)。編譯器會像
co_await
一樣調用:await_ready()
→ 決定是否掛起。await_suspend()
→ 掛起時的動作。await_resume()
→ 恢復后取值。
舉個完整例子:生成器
#include <coroutine>
#include <iostream>struct Generator {struct promise_type {int current_value;Generator get_return_object() {return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() { std::terminate(); }// **核心:co_yield 調用這個**std::suspend_always yield_value(int value) noexcept {std::cout << "[promise_type] yield_value(" << value << ")\n";current_value = value;return {}; // 掛起}};std::coroutine_handle<promise_type> handle;explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}~Generator() { if (handle) handle.destroy(); }bool move_next() {if (!handle.done()) {handle.resume();}return !handle.done();}int current_value() { return handle.promise().current_value; }
};// 協程函數
Generator numbers() {for (int i = 1; i <= 3; ++i) {co_yield i; // 調用 yield_value(i)}
}int main() {auto gen = numbers();while (gen.move_next()) {std::cout << "[main] got: " << gen.current_value() << "\n";}
}
執行流程
[promise_type] yield_value(1)
[main] got: 1
[promise_type] yield_value(2)
[main] got: 2
[promise_type] yield_value(3)
[main] got: 3
執行到 co_yield 時發生了什么?
編譯器調用
promise_type.yield_value(i)
。這個函數保存值(
current_value = i
),返回std::suspend_always
。協程掛起(
await_suspend()
)。控制權返回
main()
,用戶取值。下一次
resume()
,協程從co_yield
之后繼續。
return_value用法
co_return
在 C++20 協程中不僅可以 co_return;
,還可以 co_return value;
,對應 promise_type
的 return_value()
方法(或 return_void()
)。
co_return
用于在協程函數里 返回結果 或 結束協程。兩種形式:
co_return;
:無返回值(void
協程)。co_return expr;
:返回一個值。
編譯器會把 co_return
翻譯成 調用 promise 對象的方法:
co_return;
→promise.return_void();
co_return expr;
→promise.return_value(expr);
例子:
#include <coroutine>
#include <iostream>
#include <optional>struct Task {struct promise_type {std::optional<int> value;Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_value(int v) { value = v; } // 支持 co_return v;void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> h;explicit Task(std::coroutine_handle<promise_type> h) : h(h) {}~Task() { if (h) h.destroy(); }int get() {h.resume();return *h.promise().value;}
};Task compute() {std::cout << "Computing...\n";co_return 42; // 等價于 promise.return_value(42);
}int main() {Task t = compute();std::cout << "Result = " << t.get() << "\n";
}
協程函數不會像普通函數那樣直接“返回值”給調用者,而是:
立即返回一個包裝了
coroutine_handle
的對象(例如Task
),真正的“結果”要么通過:
promise_type
存儲(由co_return
設置值),或由外部代碼通過
handle
或包裝類(如Task::get()
)訪問。
這個std::optional<T>又是個啥?
std::optional<T>
是 C++17 引入的一個標準庫模板類,用來表示 “一個可能有值,也可能沒有值的對象”。
在協程里,它常用來延遲存儲返回值,因為:
協程一開始不會立即有返回值(因為可能掛起)。
但最終會
co_return value;
,所以我們需要一個容器,能先處于“空”狀態,等有值再存進去。
std::optional<T>
基本概念
它是一個模板類,可以包含類型
T
的值,或者為空。類似于
T*
(指針),但更安全,因為不會出現懸空指針,也不需要動態分配。常用接口:
std::optional<int> opt; // 默認無值
opt.has_value(); // 判斷是否有值
opt = 42; // 賦值
int x = *opt; // 取值(必須有值,否則未定義)
為什么用 optional 而不是直接 int?
因為協程返回
Task<int>
,在協程掛起時我們沒有值。如果直接用
int value;
,我們沒法區分“值是 0”還是“值還沒準備好”。std::optional<int>
可以:初始狀態
nullopt
(空)。return_value(42)
時賦值。之后通過
*optional
或optional.value()
取值。