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 内嵌类型指定。

一个最小协程框架需要四件套:

  1. 返回对象(Return Object):协程返回给调用者的句柄包装。
  2. promise_type:承诺对象,控制协程行为。
  3. coroutine_handle:协程句柄,用于恢复/销毁协程。
  4. Awaitable / Awaiterco_await 的操作数协议。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <coroutine>
#include <iostream>

struct Task {
struct promise_type {
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_void() {}
void unhandled_exception() { std::terminate(); }
};

std::coroutine_handle<promise_type> handle;

Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); } // RAII 销毁协程帧

void resume() { if (!handle.done()) handle(); }
};

Task simple() {
std::cout << "hello\n";
co_return; // 必须以 co_return 结尾
}

int main() {
Task t = simple(); // 创建协程,因 initial_suspend 为 suspend_always,立即挂起,未执行 body
t.resume(); // 恢复,打印 hello
}

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):

  1. await_ready():返回 booltrue 表示无需挂起直接取结果。
  2. await_suspend(handle):挂起时的动作。可返回 void(异步恢复)、bool(控制是否立即切回)、或另一个 coroutine_handle(切换到该协程)。
  3. await_resume():恢复时的返回值,即 co_await 整个表达式的值。
1
2
3
4
5
6
7
8
9
10
struct Awaiter {
bool await_ready() { return false; } // 总是挂起
void await_suspend(std::coroutine_handle<>) { /* 安排恢复 */ }
int await_resume() { return 42; } // co_await 得到 42
};

Task demo() {
int x = co_await Awaiter{}; // x == 42
co_return;
}

标准库提供两个现成 Awaitable:

  • std::suspend_always:总是挂起,await_resume() 返回 void
  • std::suspend_never:从不挂起。

生成器:co_yield 的经典应用

co_yield v 等价于 co_await promise.yield_value(v)。配合 suspend_always,可做出惰性生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <coroutine>
#include <optional>

template<typename T>
struct Generator {
struct promise_type {
T current;
Generator get_return_object() {
return {std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) { current = v; return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};

std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }

std::optional<T> next() {
if (!handle || handle.done()) return std::nullopt;
handle.resume();
if (handle.done()) return std::nullopt;
return handle.promise().current;
}
};

Generator<int> counter(int n) {
for (int i = 0; i < n; ++i)
co_yield i; // 每次产出后挂起
}

int main() {
auto gen = counter(3);
while (auto v = gen.next())
std::cout << *v << ' '; // 0 1 2
}

生成器把“状态机”隐藏在协程帧里,调用者只需 next() 拉取,无需关心内部状态。

协程帧与生命周期

调用协程函数时,编译器在堆上分配一块协程帧,保存:

  • 局部变量、参数(跨挂起点存活)。
  • promise 对象。
  • 恢复点信息。

协程帧的生命周期管理是最大的坑:

  • 悬空句柄:协程帧已销毁但句柄仍在使用 → UB。
  • final_suspend 返回 suspend_never:协程结束后自动销毁帧,此时返回对象内的句柄立即失效,再 resume()/destroy() 是 UB。故多数框架 final_suspendsuspend_always,由返回对象 RAII 销毁。
  • 对象引用局部变量:异步恢复时局部变量仍在帧里,安全;但若把指向帧内变量的指针/引用逃逸出去,协程销毁后即悬空。

分配优化

C++20 允许 operator new 优化:若编译期能确定帧大小,且 promise 提供匹配的 operator new(size_t, Args...),可在调用者栈上分配(RAII 帧)。但实践中多数协程帧仍走堆分配。

与线程、异步的关系

协程本身不引入并发——单线程内即可协程切换。它的力量在于:

  • 异步 I/Oco_await 发起 I/O 后挂起,I/O 完成时恢复,全程不阻塞线程。配合 Asio(见 [[cxx-asio]])等 reactor,可写出高吞吐异步服务。
  • 协作式调度:协程主动挂起让出,无抢占开销。

关键认知:co_await 只是“挂起 + 注册恢复”,真正的并发来自事件循环 / 线程池在恢复时的调度。没有调度器,co_await 会一直挂起不返回。

一个伪异步示例

展示挂起/恢复机制本身(不涉及真实异步 I/O):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct WaitOneFrame {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 把恢复动作排到"稍后"——这里用 std::thread 模拟
std::thread([h]{ h.resume(); }).detach();
}
void await_resume() {}
};

Task loop() {
std::cout << "start\n";
co_await WaitOneFrame{}; // 挂起,由另一个线程恢复
std::cout << "resumed\n";
co_return;
}

真实场景中 await_suspend 会把句柄注册到事件循环,I/O 就绪时回调 h.resume()

与其他语言协程的区别

  • Go goroutine:抢占式、有运行时调度器、栈可增长。C++ 协程无运行时,需库提供调度。
  • Python/JS async/await:语言级 async 函数 + 事件循环。C++ 把调度权完全交给库,更灵活但更繁琐。
  • Rust async:零成本、基于 Future 的状态机。C++ 协程帧通常堆分配,更易用但开销略高。

常见陷阱

  1. 忘记 final_suspend 挂起:协程自销毁后返回对象句柄悬空。
  2. 跨线程恢复不加同步:句柄被多线程 resume() 是数据竞争。
  3. co_await 一个非 Awaitable:编译错误,提示晦涩,需检查三方法。
  4. 协程返回 void 而非 Task:协程返回类型必须是协程返回对象,裸 void 不行。
  5. 以为协程自动并发:没有调度器/事件循环,挂起的协程永远不会恢复。
  6. 协程内捕获 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]]。