C++ 专家(18):协程机制深入——awaitable、promise 类型、对称传输、调度器实现
更新时间:2026-09-01。本文是
languages/cpp/expert/专家层第 18 篇,接 线程同步深入。C++20 协程是编译器生成的机器码,不是运行时。理解协程的底层机制——promise_type、awaitable、handle、最终挂起——才能写出高性能的协程代码。
本文要回答的问题
- promise_type 的完整接口是什么?必须实现哪些方法?
- awaitable 类型怎么实现?await_ready、await_suspend、await_resume 的交互?
- 对称传输(symmetric transfer)是什么?怎么避免栈溢出?
- 协程的性能开销在哪里?什么时候该用协程?
一、promise_type 完整接口
cpp
struct promise_type {
// 1. 必须:返回协程返回类型
auto get_return_object();
// 2. 必须:初始挂起行为
std::suspend_always initial_suspend(); // 一开始就挂起
// std::suspend_never initial_suspend(); // 一开始就执行
// 3. 必须:最终挂起行为
std::suspend_always final_suspend() noexcept; // 结束后挂起
// 4. 必须:异常处理
void unhandled_exception();
// 5. 可选:co_return 时调用
void return_void(); // co_return; 无返回值
// void return_value(T v); // co_return v; 有返回值
// 6. 可选:co_yield 时调用
// std::suspend_always yield_value(T v);
// 7. 可选:co_await 转换
// auto await_transform(T v);
};二、awaitable 类型
cpp
// 一个完整的 awaitable 类型
struct AsyncOperation {
int result;
// await_ready:准备好了吗?
// true → 不挂起,直接执行 await_resume
// false → 挂起,调用 await_suspend
bool await_ready() const noexcept {
return false; // 还没准备好,要挂起
}
// await_suspend:挂起后做什么?
// 返回 void → 挂起后不做什么
// 返回 bool → true 继续挂起,false 恢复
// 返回 coroutine_handle → 恢复另一个协程(对称传输)
void await_suspend(std::coroutine_handle<> handle) {
// 保存 handle,在异步操作完成后调用 handle.resume()
schedule_async_work([handle]() {
handle.resume();
});
}
// await_resume:恢复时返回什么?
int await_resume() const noexcept {
return result; // co_await 表达式的结果
}
};三、对称传输
cpp
// 对称传输:避免深嵌套 resume 导致栈溢出
// 假设协程 A 恢复协程 B,B 恢复 C,C 恢复 A...
// ❌ 非对称传输:递归 resume,可能栈溢出
void await_suspend(std::coroutine_handle<> handle) {
// 直接 resume 另一个协程
other_handle.resume(); // 在 await_suspend 的栈上 resume
// 如果链很长,栈会爆
}
// ✅ 对称传输:返回另一个协程的 handle,编译器做 tail resume
std::coroutine_handle<> await_suspend(std::coroutine_handle<> handle) {
// 返回另一个协程的 handle,编译器会做尾调用
return other_handle;
// 当前协程的栈帧被销毁,然后恢复 other_handle
// 不会栈溢出
}四、协程的堆分配
cpp
// 协程帧默认在堆上分配
// 编译器会调用 promise_type 的 operator new 或全局 operator new
struct promise_type {
// 自定义分配
void* operator new(size_t size) {
return pool.allocate(size);
}
void operator delete(void* ptr, size_t size) {
pool.deallocate(ptr, size);
}
};
// 编译器优化:如果生命周期在调用者内,可以省略堆分配
{
auto gen = fib(10); // 协程帧可能分配在栈上
// 如果编译器确定生命周期不超出作用域
}五、协程性能分析
| 操作 | 开销 | 说明 |
|---|---|---|
| 创建协程 | 1 次堆分配 | 协程帧分配 |
| co_await 挂起 | 几个函数调用 | await_ready/await_suspend |
| 恢复协程 | 1 次函数调用 | handle.resume() |
| 销毁协程 | 1 次堆释放 | 协程帧释放 |
对比线程: 协程的创建/切换开销是纳秒级,线程是微秒级。协程更适合大量并发任务。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 忘记 final_suspend 挂起 | 协程帧提前销毁 | final_suspend 返回 suspend_always |
| 对称传输不用 | 深嵌套栈溢出 | await_suspend 返回 coroutine_handle |
| 协程帧堆分配 | 内存碎片 | 自定义 promise_type 的 operator new |
| 销毁后还 resume | UB | 检查 handle.done() |
相关与延伸
下一篇:constexpr 一切——编译期计算、constexpr 容器、consteval;协程入门,见 协程实战。
一句话总结
C++20 协程机制深入:promise_type 定义协程行为(initial_suspend、final_suspend、return_value、yield_value);awaitable 实现 await_ready(是否挂起)、await_suspend(挂起后动作)、await_resume(恢复时返回值);对称传输通过 await_suspend 返回 coroutine_handle 做尾调用,避免深递归栈溢出;协程帧默认堆分配,可自定义 operator new 优化;协程切换开销纳秒级,比线程轻量得多。