C++ Coroutines
C++20 引入的协程(Coroutines)是一种可暂停、可恢复的函数。普通函数从入口跑到结尾一次性完成;协程可以在中途挂起(suspend),把控制权交还调用者,之后从挂起点继续执行。
类比:普通函数是一段单次播放的录音;协程是可以暂停、续播、还能边播边产值的播放器。
核心价值:用同步的线性代码风格写出异步逻辑,避免回调地狱与状态机手写。但 C++20 只提供“协程机制”(编译器变换 + 库支持),不提供开箱即用的异步框架——这是 C++ 协程学习曲线陡峭的根源。
三个关键字
C++ 协程由三个新关键字驱动,任一函数出现它们即被编译器识别为协程:
| 关键字 | 作用 |
|---|---|
co_await expr | 挂起当前协程,等待一个 Awaitable 完成,再取回结果恢复 |
co_yield expr | 产出一个值并挂起(生成器模式) |
co_return expr | 协程结束,返回最终值 |
协程不能有普通 return,返回类型也必须是协程返回对象(如 Task<T>、generator<T>),而非裸值。
协程的骨架:promise_type
协程函数被编译器变换后,关键是一个承诺对象(promise object),它定义了协程如何被创建、挂起、产出和销毁。承诺对象的类型由返回对象通过 promise_type 内嵌类型指定。
一个最小协程框架需要四件套:
- 返回对象(Return Object):协程返回给调用者的句柄包装。
- promise_type:承诺对象,控制协程行为。
- coroutine_handle:协程句柄,用于恢复/销毁协程。
- Awaitable / Awaiter:
co_await的操作数协议。
1 | |
promise_type 的钩子
| 钩子 | 何时调用 | 作用 |
|---|---|---|
get_return_object() | 创建协程帧后 | 构造返回给调用者的对象 |
initial_suspend() | body 执行前 | 是否在开始处挂起(suspend_always = 惰性启动) |
final_suspend() | body 结束后 | 是否在结尾挂起(通常 suspend_always,避免句柄悬空) |
return_void() / return_value(v) | co_return 时 | 接收最终结果 |
yield_value(v) | co_yield 时 | 接收产出的值 |
unhandled_exception() | 异常逃逸时 | 处理未捕获异常 |
coroutine_handle
std::coroutine_handle<Promise> 是协程帧的非拥有句柄(类似裸指针),核心操作:
handle()或handle.resume():恢复协程。handle.done():是否执行到co_return。handle.destroy():销毁协程帧(释放内存)。handle.promise():访问 promise 对象。std::coroutine_handle<Promise>::from_promise(p):由 promise 反推句柄。
句柄不拥有协程帧——谁负责
destroy()取决于设计。常见做法:返回对象析构时destroy()(RAII),或协程final_suspend中自销毁。
co_await 与 Awaitable 协议
co_await expr; 中 expr 须是一个 Awaitable。编译器对它调用三个方法(组成 Awaiter):
await_ready():返回bool。true表示无需挂起直接取结果。await_suspend(handle):挂起时的动作。可返回void(异步恢复)、bool(控制是否立即切回)、或另一个coroutine_handle(切换到该协程)。await_resume():恢复时的返回值,即co_await整个表达式的值。
1 | |
标准库提供两个现成 Awaitable:
std::suspend_always:总是挂起,await_resume()返回void。std::suspend_never:从不挂起。
生成器:co_yield 的经典应用
co_yield v 等价于 co_await promise.yield_value(v)。配合 suspend_always,可做出惰性生成器:
1 | |
生成器把“状态机”隐藏在协程帧里,调用者只需 next() 拉取,无需关心内部状态。
协程帧与生命周期
调用协程函数时,编译器在堆上分配一块协程帧,保存:
- 局部变量、参数(跨挂起点存活)。
- promise 对象。
- 恢复点信息。
协程帧的生命周期管理是最大的坑:
- 悬空句柄:协程帧已销毁但句柄仍在使用 → UB。
- final_suspend 返回 suspend_never:协程结束后自动销毁帧,此时返回对象内的句柄立即失效,再
resume()/destroy()是 UB。故多数框架final_suspend用suspend_always,由返回对象 RAII 销毁。 - 对象引用局部变量:异步恢复时局部变量仍在帧里,安全;但若把指向帧内变量的指针/引用逃逸出去,协程销毁后即悬空。
分配优化
C++20 允许 operator new 优化:若编译期能确定帧大小,且 promise 提供匹配的 operator new(size_t, Args...),可在调用者栈上分配(RAII 帧)。但实践中多数协程帧仍走堆分配。
与线程、异步的关系
协程本身不引入并发——单线程内即可协程切换。它的力量在于:
- 异步 I/O:
co_await发起 I/O 后挂起,I/O 完成时恢复,全程不阻塞线程。配合 Asio(见 [[cxx-asio]])等 reactor,可写出高吞吐异步服务。 - 协作式调度:协程主动挂起让出,无抢占开销。
关键认知:
co_await只是“挂起 + 注册恢复”,真正的并发来自事件循环 / 线程池在恢复时的调度。没有调度器,co_await会一直挂起不返回。
一个伪异步示例
展示挂起/恢复机制本身(不涉及真实异步 I/O):
1 | |
真实场景中 await_suspend 会把句柄注册到事件循环,I/O 就绪时回调 h.resume()。
与其他语言协程的区别
- Go goroutine:抢占式、有运行时调度器、栈可增长。C++ 协程无运行时,需库提供调度。
- Python/JS async/await:语言级
async函数 + 事件循环。C++ 把调度权完全交给库,更灵活但更繁琐。 - Rust async:零成本、基于 Future 的状态机。C++ 协程帧通常堆分配,更易用但开销略高。
常见陷阱
- 忘记
final_suspend挂起:协程自销毁后返回对象句柄悬空。 - 跨线程恢复不加同步:句柄被多线程
resume()是数据竞争。 co_await一个非 Awaitable:编译错误,提示晦涩,需检查三方法。- 协程返回 void 而非 Task:协程返回类型必须是协程返回对象,裸
void不行。 - 以为协程自动并发:没有调度器/事件循环,挂起的协程永远不会恢复。
- 协程内捕获
this:若this在协程恢复前销毁,UB。可用co_await前复制需要的值。
小结
- 协程 = 可暂停可恢复的函数,由
co_await/co_yield/co_return标记。 - 骨架是
promise_type,控制创建、挂起、产出、销毁。 coroutine_handle是非拥有句柄,生命周期需谨慎管理(RAII +final_suspend挂起)。co_await的语义由 Awaitable 的await_ready/suspend/resume三方法决定。- 生成器是
co_yield的经典用法;异步 I/O 是co_await的主战场(配合 [[cxx-asio]])。 - C++20 只给机制不给框架——生产用需选 Asio 协程、cppcoro/CppTask 等库。
要真正落地协程,下一步是理解 [[cxx-asio]] 的协程用法与一个完整调度器实现。模板元编程基础见 [[cxx-templates]]。







