详解C++20的协程
C++20 协程是标准里最复杂的特性之一,设计目标是”零开销可定制”,代价是学习曲线极陡。下面从底层机制到实际用法系统讲清楚。
一、什么是 C++20 协程
C++20 的协程是无栈协程(stackless coroutine)。区别在于:
- 有栈协程(如 Boost.Coroutine、goroutine):每个协程有自己的调用栈,可以在任意深度的函数调用中间暂停
- 无栈协程:暂停点只能出现在协程函数本身的顶层,局部变量保存在堆上的协程帧里,没有独立调用栈
无栈协程的内存开销远小于有栈协程(通常几十字节 vs 数 KB),但表达能力稍有限制。
一个函数只要用了 co_await、co_yield、co_return 三个关键字之一,它就是协程。
二、三个关键字
co_await expr; // 暂停协程,等待 expr 完成 co_yield value; // 暂停协程,向调用者产出一个值 co_return value; // 结束协程,返回最终结果
注意:协程函数的返回类型不是 int、string 这类普通类型,而是一个特殊的 Promise 类型(下文详解)。
三、编译器做了什么
这是理解 C++20 协程的关键。当你写下一个协程函数,编译器会把它变换成大量”看不见”的代码。
假设你写:
Task<int> compute() {
int x = co_await fetch_data();
co_return x * 2;
}
编译器大致会把它变换成:
// 伪代码,展示编译器的变换逻辑
Task<int> compute() {
// 1. 在堆上分配协程帧,存放局部变量和暂停状态
auto* frame = operator new(sizeof(ComputeFrame));
// 2. 构造 promise 对象(位于协程帧内)
promise_type promise;
// 3. 拿到返回给调用者的对象
Task<int> result = promise.get_return_object();
// 4. 执行初始暂停(如果 initial_suspend 返回 suspend_always)
co_await promise.initial_suspend();
// ---- 协程体开始 ----
int x = co_await fetch_data(); // 暂停点
promise.return_value(x * 2); // co_return 展开
// ---- 协程体结束 ----
// 5. 执行最终暂停
co_await promise.final_suspend();
// 6. 销毁协程帧
frame->destroy();
return result;
}
关键在于:result 在步骤 3 就返回给调用者了,此时协程体甚至还没开始执行(取决于 initial_suspend 的策略)。
四、协程帧(Coroutine Frame)
每个协程实例在堆上有一个协程帧,包含:
- 所有局部变量(在暂停点之间存活的那些)
promise_type对象- 当前暂停点的索引(实现为一个状态机)
- 协程函数的参数副本
编译器会把协程体变成一个状态机,每次 resume() 从上次暂停的地方继续执行:
状态 0 → 执行到 co_await fetch_data() → 暂停 → 状态 1 状态 1 → fetch_data() 完成,resume() 被调用 → 执行 co_return → 状态 2 (done)
五、四个核心定制点
C++20 协程的”复杂”主要来自这里——标准只提供机制,不提供策略,所有行为都通过这四个东西定制。
1. Promise 类型(promise_type)
协程的返回类型 T 必须有一个嵌套类型 T::promise_type(或通过 coroutine_traits 特化指定),它控制协程的生命周期行为:
struct MyPromise {
// 必须:返回给调用者的对象
Task<int> get_return_object();
// 必须:协程刚创建时是否立即暂停
// suspend_always → 懒启动;suspend_never → 立即开始执行
auto initial_suspend() { return std::suspend_always{}; }
// 必须:协程结束时是否暂停(控制谁销毁协程帧)
auto final_suspend() noexcept { return std::suspend_always{}; }
// 必须:处理未捕获异常
void unhandled_exception() { std::terminate(); }
// co_return value 时调用
void return_value(int v) { result = v; }
// co_yield value 时调用(返回一个 awaitable)
auto yield_value(int v) {
result = v;
return std::suspend_always{};
}
int result;
};
2. Awaitable 与 Awaiter
co_await expr 中的 expr 必须是一个 Awaitable。编译器会把它变换成:
auto&& awaitable = expr;
// 先尝试从 awaitable 获取 awaiter(可以是 awaitable 自身)
auto&& awaiter = awaitable.operator co_await();
// 或者 operator co_await(awaitable)
// 然后执行三步协议:
if (!awaiter.await_ready()) { // 是否已经就绪?
awaiter.await_suspend(handle); // 暂停,handle 是当前协程的句柄
// ... 控制权返回给调用者 ...
}
auto result = awaiter.await_resume(); // 恢复时拿到结果
所以自定义 awaiter 需要实现三个方法:
struct MyAwaiter {
bool await_ready() { return false; } // false = 需要暂停
void await_suspend(std::coroutine_handle<> handle) {
// 在这里:把 handle 交给某个异步操作
// 当异步操作完成时,调用 handle.resume() 恢复协程
async_op.on_complete([handle]{ handle.resume(); });
}
int await_resume() { return result; } // 暂停结束后的返回值
int result;
};
await_suspend 还可以返回 bool(false 则不暂停)或另一个 coroutine_handle<>(对称转移,避免栈溢出)。
3. coroutine_handle<P>
std::coroutine_handle<P> 是协程的句柄,本质上是指向协程帧的指针。它的核心操作:
handle.resume(); // 恢复协程执行 handle.destroy(); // 销毁协程帧,释放内存 handle.done(); // 是否已经结束 handle.promise(); // 访问 promise 对象
coroutine_handle<void> 是类型擦除版本,可以指向任何协程。
4. std::suspend_always 和 std::suspend_never
这两个是标准提供的最简单的 awaiter:
struct suspend_always {
bool await_ready() { return false; } // 总是暂停
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
struct suspend_never {
bool await_ready() { return true; } // 从不暂停
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
六、从零实现一个 Generator
用协程实现惰性序列生成器,是最直观的入门案例:
#include <coroutine>
#include <optional>
template<typename T>
class Generator {
public:
struct promise_type {
std::optional<T> value;
Generator get_return_object() {
return Generator{Handle::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
std::suspend_always yield_value(T v) {
value = std::move(v);
return {};
}
};
using Handle = std::coroutine_handle<promise_type>;
explicit Generator(Handle h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// 不可拷贝,可移动
Generator(const Generator&) = delete;
Generator(Generator&& o) : handle(o.handle) { o.handle = {}; }
std::optional<T> next() {
if (!handle || handle.done()) return std::nullopt;
handle.resume();
if (handle.done()) return std::nullopt;
return handle.promise().value;
}
private:
Handle handle;
};
// 使用:
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto c = a + b;
a = b;
b = c;
}
}
int main() {
auto gen = fibonacci();
for (int i = 0; i < 10; i++) {
std::cout << *gen.next() << " ";
}
// 输出: 0 1 1 2 3 5 8 13 21 34
}
七、异步任务:实现 Task
异步场景下,Task<T> 表示一个懒启动的单次异步操作:
template<typename T>
class Task {
public:
struct promise_type {
std::variant<std::monostate, T, std::exception_ptr> result;
std::coroutine_handle<> continuation; // 等待我的那个协程
Task get_return_object() {
return Task{Handle::from_promise(*this)};
}
// 懒启动:创建后不立即执行
std::suspend_always initial_suspend() { return {}; }
// 结束时暂停,然后恢复等待者
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
auto await_suspend(Handle h) noexcept {
// 对称转移:直接跳转到 continuation,不增加调用栈
return h.promise().continuation;
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
void return_value(T v) { result = std::move(v); }
void unhandled_exception() { result = std::current_exception(); }
};
using Handle = std::coroutine_handle<promise_type>;
// Task 本身是一个 Awaitable
bool await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> caller) {
handle.promise().continuation = caller;
return handle; // 对称转移到 task 的协程
}
T await_resume() {
auto& result = handle.promise().result;
if (std::holds_alternative<std::exception_ptr>(result))
std::rethrow_exception(std::get<std::exception_ptr>(result));
return std::move(std::get<T>(result));
}
// ... 构造、析构、移动语义省略 ...
private:
Handle handle;
};
// 使用:
Task<int> fetch_user_id(std::string name) {
auto data = co_await http_get("/users/" + name);
co_return data["id"].as<int>();
}
Task<std::string> get_username() {
int id = co_await fetch_user_id("alice");
auto info = co_await http_get("/profiles/" + std::to_string(id));
co_return info["name"].as<std::string>();
}
八、对称转移(Symmetric Transfer)
这是 C++20 协程的一个关键技术细节,解决的是栈溢出问题。
没有对称转移时:
main → resume(task_A) → task_A 结束 → resume(task_B) → task_B 结束 → ...
每次 resume 都是一次函数调用,链式调用会撑爆调用栈。
await_suspend 返回 coroutine_handle<> 时,编译器会把它变成”尾调用跳转”:
auto await_suspend(coroutine_handle<> h) -> coroutine_handle<> {
return next_coro; // 直接跳转,不增加栈帧
}
这是 C++20 协程能高效实现 continuation-passing style 的基础。
九、内存分配优化
协程帧默认用 operator new 分配。但编译器有一个重要优化:协程帧省略(Heap Allocation Elision Optimization, HALO)——如果编译器能确定协程的生命周期不超过调用者,它可以把协程帧分配在调用者的栈上,彻底消除堆分配。
这个优化在实践中常见于简单的 generator 场景,但在跨越异步边界时通常无法触发。
自定义分配可以在 promise_type 里重载 operator new:
struct promise_type {
void* operator new(size_t size, MyAllocator& alloc, ...) {
return alloc.allocate(size);
}
void operator delete(void* ptr, size_t size) {
MyAllocator::deallocate(ptr, size);
}
};
十、标准库提供了什么,没提供什么
提供了:
std::coroutine_handle<P>— 协程句柄std::coroutine_traits<R, Args...>— 定制 promise_type 的特化点std::suspend_always、std::suspend_never— 两个基础 awaiterstd::noop_coroutine()— 用于对称转移链的终止节点
没有提供:
Task<T>、Generator<T>、AsyncGenerator<T>— 你要自己写或用库- 线程池调度器
- 任何异步 I/O 集成
这是 C++20 协程被诟病最多的一点:标准只给了机制,没给工具。你拿到的是发动机零件,不是一辆车。
十一、实际上用什么
手写 Task/Generator 非常繁琐且容易出错,实际项目中通常用:
- cppcoro(Lewis Baker 写的,是 P2300 的精神前身,已停止维护但代码质量极高,值得学习)
- libunifex(Meta,P2300 的另一个参考实现,与 sender/receiver 集成)
- Asio(Boost.Asio 和 standalone Asio 都支持
co_await,是目前生产可用度最高的选项) - cpptask / folly::coro(Facebook 内部,有开源版)
- C++26 的
std::generator(标准终于提供了一个 generator,不过只是 C++26)
十二、与 Rust async/await 的对比
| C++20 协程 | Rust async/await | |
|---|---|---|
| 协程类型 | 无栈 | 无栈 |
| 运行时 | 无内置运行时,完全定制 | 无内置运行时,需要 tokio/async-std |
| 开销 | 理论零开销,实践中取决于实现 | 类似 |
| 学习曲线 | 极陡,需理解 promise/awaiter 协议 | 较平,async fn 自动推导 Future |
| 生态 | 碎片化,无统一 executor | tokio 一统江湖 |
| 错误处理 | 手动,unhandled_exception |
? 运算符天然支持 |
| 取消 | 无标准机制 | 无标准机制,tokio 有 CancellationToken |
C++ 协程的定制能力比 Rust 强得多(你可以完全控制调度策略和内存分配),但这种自由度的代价是极高的复杂性。Rust 选择了一个更窄但更好用的点。
一句话总结
C++20 协程是一套可定制的编译期变换机制,把协程函数变成状态机,把局部状态存到堆上的协程帧里。它本身极其灵活,但几乎不提供开箱即用的工具——真正拿来用还需要一个成熟的上层库(目前 Asio 是最现实的选择),或者等 C++26/C++29 标准库逐步补全配套设施。
