Appearance
C++20 协程(Coroutines):co_await / co_return / co_yield
HPC/AI Infra 里协程常用于高并发 IO、异步流水线、generator(事件循环、网络、文件背压)。面试考:协程是什么、co_await 机制、性能特征、与线程/无栈/有栈对比、手写 promise/awaitable 概念。
一、协程是什么
协程 = 可暂停/恢复的函数。C++20 协程是无栈协程:暂停时把状态(局部变量、恢复点)存入堆上的帧(frame),恢复后从断点继续——不占系统线程、无需上下文切换,开销极小。
三种新关键字:
co_await expr:暂停等待某个 awaitable 就绪后恢复;co_return value:结束并返回;co_yield v:产出一个值并暂停(generator 场景)。
函数体一旦含任一关键字即成为协程(返回类型要满足协程协议:有 promise_type)。
二、背后机制(面试考察核心)
写一个协程类型时编译器会生成:
- 协程帧 frame(在堆上):存 promise 对象、参数拷贝、局部变量、恢复点(suspend 点地址/索引)。
- promise_type:决定协程生命周期钩子(
get_return_object、initial_suspend、final_suspend、unhandled_exception、return_value/return_void/yield_value)。 - 三个 suspend 点:
initial_suspend:是否在最开始就暂停(暂停=惰性,等外部 resume;不暂停=启动即执行)。await_suspend:co_await 处,返回 true/void 表示暂停并注册回调;false 表示不暂停(直接继续)。final_suspend:结束前最后一个点(通常 suspend 让外部安全接管返回值;也可对称恢复)。
cpp
// 一个最小可 co_await 类型必须提供:
struct Awaitable {
bool await_ready() const noexcept; // 是否已就绪(true 就不挂)
void await_suspend(std::coroutine_handle<>) const noexcept{} // 决定是否挂/做什么
int await_resume() const noexcept; // co_await 表达式的值
};生命周期速查
- 外部持
std::coroutine_handle<P>(句柄)逐步resume()驱动; handle.destroy()释放帧;RAII 包装(像 std::future 的 promise)负责自动释放,防止泄漏。
三、最小示例(generator 概念,能讲清即加分)
cpp
#include <coroutine>
struct Generator {
struct promise_type {
int current;
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 {}; }
std::suspend_always yield_value(int v) noexcept { current = v; return {}; }
void return_void() noexcept {}
void unhandled_exception() noexcept { std::terminate(); }
};
using Handle = std::coroutine_handle<promise_type>;
explicit Generator(Handle h) : h_(h) {}
~Generator(){ if (h_) h_.destroy(); } // RAII:释放协程帧
bool next(){ h_.resume(); return !h_.done(); }
int value() const { return h_.promise().current; }
private:
Handle h_;
};
Generator range(int n) {
for (int i = 0; i < n; ++i)
co_yield i; // 产出并暂停
}
// 用法: while(g.next()) use(g.value());四、高频问答
Q1. 协程 vs 线程
| 协程 | 线程 | |
|---|---|---|
| 调度 | 用户态手动/事件循环驱动 | OS 内核调度 |
| 切换开销 | 极低(保存少量寄存器/读帧) | 上下文切换(μs 级)+ 内核态 |
| 栈 | 无栈——不占独立线程栈,可开海量 | 每线程独立栈(~8MB 虚) |
| 并行的多核利用 | 通常单线程事件循环内并发(不并行真正计算) | 多核并行 |
| 适用 | 大量等待/IO、背压流水、状态机 | CPU 密集型要真并行 |
HPC 结合:协程是「并发不并行」——想用满多核跑算子仍要线程 + 任务;协程常用于把 异步 IO / 顺序状态机写得很贴近同步代码。
Q2. 与有栈协程(如 Boost.Context / ucontext)差异?
- 无栈:unwinder 不需要保存整个调用栈、可开海量、可与 C 调用栈混用;但不能在一个无栈协程里轻易切到另一个(除非用对称转移 hack)。
- 有栈:可任意嵌套切换、像线程控制;成本略高、伸缩性略差。C++20 是无栈标准。
Q3. 为什么说协程是「一个返回可恢复对象的函数」而非「轻线程」?
因为没有独立的调用栈——它把状态塞进自己的 frame;调度由外部 handle resume 驱动,语义上更像「惰性生成器/异步 task」而不是被抢占的线程。
Q4. co_await 一个 future / IO 回调怎么展开(异步化模板)?
- await 一个「还没完成的 IO」→ await_ready 返回 false;
- await_suspend 里把协程 handle 注册为 IO 完成回调(勿自持有句柄导致悬垂/destroy 泄漏);
- IO 完成后在该 IO 线程/事件循环调用 handle.resume() → 协程从 co_await 处继续。
- 危险:resume 所在线程可能与发起线程不同;帧生命周期管理(谁 destroy)必须在设计中明确(RAII task + 完成即释放)。
Q5. 性能 / 该不该处处用协程?
- 开销主要是帧的堆分配(造一次分配 + resume 微成本)。热路径可用自定义分配器把帧放进预分配池。
- 滥用(把 CPU 短任务套协程)反而差;**只在上古「异步 IO 回到同步体验 / 大量并发连接」**时收益最大。
五、工程注意 & 常见坑
- 不要返回局部/被捕获对象在协程暂停后失效——协程跨越生命周期,捕获
this/引用进协程帧若对象已死 → 悬垂。常见解法 task 所有权随 IO。 - 忘记
h_.destroy()→ 帧泄漏(RAII 包装必须)。 initial_suspend决定惰性/立即;final_suspend位置影响能否在结束后安全 read promise。- 对称暂停(对称转移)对自实现复杂链式协程较少需要,能提即懂深浅。
- 现成库:folly::coro、asio awaitable、cppcoro——面试讲「会基于框架用 coro + asio 做异步」足够。
六、一句话总结给面试官
「C++20 协程是无栈协程:函数暂停时把局部状态存进堆上帧,用 handle 驱动 resume;核心收益是以同步代码写异步,避免为海量等待各占线程;代价是帧分配与正确的生命周期/调度管理,真并行仍靠线程+任务。」