Part 1: 基础概念与历史
1.1 什么是协程?
协程(Coroutines)是一种程序组件,它允许函数在执行过程中暂停(suspend)和恢复(resume),而无需丢失其局部状态。不同于传统函数的“调用-返回”模型,协程可以多次进入和退出,类似于线程但更轻量级。
- 关键特点:
- 协作式多任务:协程不会被抢占(preempted),而是主动挂起,效率高。
- 无栈/有栈:C++协程是无栈(stackless)的,即不分配独立栈帧,而是通过状态机模拟。
- 异步友好:特别适合I/O-bound任务,如网络请求、文件读写。
与线程的区别:
- 线程:OS级,抢占式,切换开销大(上下文切换)。
- 协程:用户级,协作式,开销小(几字节状态)。
有栈协程和无栈协程
| 维度 | 有栈协程 (Stackful) | 无栈协程 (Stackless) |
|---|---|---|
| 挂起时是否保留独立调用栈 | 有(每个协程有自己的栈) | 没有(栈帧被“压平”成状态机) |
| 能否在任意嵌套深度挂起 | 可以(任意函数、任意调用层级) | 不行(只能在协程函数的顶层直接挂起点) |
| 实现方式 | 保存/切换整个栈 + 寄存器上下文 | 编译器把函数改写成状态机 + 保存局部变量 |
| 代表语言/运行时 | Lua、Go(goroutine)、Python 2.x greenlet、Boost.Coroutine、某些企业级VM | C++20、Python 3 async/await、JavaScript async/await、Kotlin协程、C# async/await、Rust async |
| 内存模型 | 每个协程预分配或动态增长的栈(通常几KB~几MB) | 每个协程只分配固定大小的帧(几十字节~几KB) |
| 上下文切换开销 | 较高(拷贝栈 + 切换栈指针) | 极低(跳转 + 恢复几个寄存器/成员变量) |
| 创建一个协程 | 10–100 μs(栈分配) | 0.1–2 μs |
| 上下文切换一次 | 50–300 ns | 5–30 ns |
| 单个协程挂起时内存 | 4KB–8MB(可增长) | 40B–2KB(固定) |
| 10万个协程内存 | 400MB–数百GB(灾难) | 几MB–几十MB |
| 栈溢出风险 | 有(栈大小固定或增长慢) | 无(没有栈) |
-
有栈协程本质上是“用户态的轻量线程”,实现方式基本都围绕“上下文切换 + 独立栈”。
主流实现手段
- setjmp / longjmp(最古老、最 portable 的方式)
- setjmp 保存当前调用栈 + 寄存器快照到 jmp_buf
- longjmp 恢复快照,跳转回去
- 缺点:只能单向跳转,不能反复切换
- ucontext / swapcontext(POSIX 标准,Solaris / BSD / Linux glibc 都有)
- makecontext:创建一个新上下文(栈 + 入口函数)
- swapcontext:原子切换两个上下文(保存旧的,加载新的)
- 这是最经典的有栈协程上下文切换原语
- 代表库:Boost.Coroutine(早期版本)、libco、libcopp、go 的部分实现灵感
- 汇编手写上下文切换(最高性能做法)
- 只保存/恢复真正用到的寄存器(而不是整个上下文)
- 典型保存内容:rsp(栈指针)、rbp、rbx、r12~r15、rip(指令指针)
- 现代高性能库(libco、PhotonLibOS、腾讯的 libco)几乎都用手写汇编
- 开销可以压到 20~50 纳秒(x86-64)
- 栈的分配策略
- 固定大小栈(4KB~8MB)
- 动态增长栈(像 Go 那样,初始 2KB,溢出时拷贝到更大栈)
- 保护页(guard page)防止栈溢出
- setjmp / longjmp(最古老、最 portable 的方式)
-
无栈协程的实现完全依赖编译器做源到源(或中端)变换,把协程函数改写成一个状态机对象。
原始代码:
1 2 3 4 5 6 7
Task<int> f() { int x = 1; co_await io_op(); // 挂起点1 int y = x + 10; co_await timer(100ms); // 挂起点2 co_return y * 2; }
编译器变换后大致变成:
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
struct f_promise { /* ... */ }; struct Frame_f { int state = 0; // 当前状态编号 int x; // 跨挂起点的变量提升为成员 int y; // 其他 promise / awaitable 相关字段 }; void f_resume(Frame_f* frame) { switch (frame->state) { case 0: // 初始进入 frame->x = 1; frame->state = 1; if (!io_op.await_ready()) { io_op.await_suspend(handle_from_frame(frame)); return; // 挂起,返回控制权 } // 如果已经 ready,直接 fall-through case 1: // 从第一个 co_await 恢复 io_op.await_resume(); // 取结果(如果有) frame->y = frame->x + 10; frame->state = 2; if (!timer(100ms).await_ready()) { timer(100ms).await_suspend(handle_from_frame(frame)); return; } case 2: timer(100ms).await_resume(); frame->promise.return_value(frame->y * 2); // 结束 } }
- 所有跨 co_await 的局部变量被提升为 Frame 的成员
- 每个 co_await / co_yield 变成一个状态 case
- 挂起时只保存:状态编号 + 成员变量(通常几十~几百字节)
- 没有栈切换,只有普通函数返回 + 后续 resume 时 switch 跳转
两者最直观的差异对比表
维度 有栈协程实现方式 无栈协程实现方式 挂起时保存的数据量 几十~几百字节(寄存器 + 栈内容) 通常几十字节(状态 + 必要变量) 保存谁的栈 每个协程独立栈(完整调用链) 没有独立栈,用调用者的栈或堆对象 能否在深层嵌套函数 yield 是 否(除非编译器做静态展开/合并) 编译器复杂度 低(运行时实现) 极高(类型系统、借用检查、状态机生成、优化) 运行时复杂度 高(上下文切换、栈管理、增长) 低(几乎就是普通函数调用) 优化难度 较难(运行时行为) 容易(静态状态机,容易内联、死代码消除) 典型代表(2026视角) Go goroutine、libco、Boost.Coroutine、Lua C++20 co_await、Rust async、JS await、Kotlin suspend
1.2 协程的历史
协程概念最早由Melvin Conway在1963年提出,用于汇编语言的子程序。现代协程在Lua、Python、Go等语言中流行。
- C++中的演变:
- Pre-C++11:无原生支持,依赖库如Boost.Coroutine(有栈协程)。
- C++20:引入无栈协程,基于提案N4402/N4628。
- C++23:添加std::generator、std::stacktrace等辅助。
- C++使用无栈协程追求零开销抽象,避免栈分配的性能损失。
C++协程的设计哲学:提供最小化语言支持,留给库作者灵活扩展。这导致学习曲线陡峭,但潜力巨大。
1.3 为什么需要协程?
传统异步编程痛点:
- 回调地狱(Callback Hell):嵌套回调导致代码难以阅读。
- Promise/Future:C++11引入,但链式调用仍复杂。
- 线程:并发开销大,不适合高并发I/O。
协程解决:用同步风格写异步代码,如co_await等待操作完成,代码线性易读。
示例场景:
- 网络服务器:处理千级连接,无需线程池。
- 生成器:惰性产生序列(如无限斐波那契)。
- 模拟器:游戏AI、物理模拟的暂停/恢复。
Part 2: 核心语法与关键字
C++20协程的核心是三个关键字:co_await、co_yield、co_return。函数中出现任一,即为协程函数。
2.1 co_await:等待操作
co_await expr;:挂起协程,直到expr(awaitable对象)就绪。
- 语义:如果expr立即就绪,不挂起;否则挂起,待恢复时继续。
- awaitable要求:需实现operator co_await()或await_transform。
简单示例(假设有Awaitable):
1
2
3
4
MyCoroutine async_task() {
auto result = co_await some_async_op(); // 挂起直到op完成
std::cout << result << std::endl;
}
2.2 co_yield:产生值并挂起
co_yield expr;产生expr值给调用者,然后挂起协程。
- 用途:实现生成器(generator),如迭代序列。
- 语义:每次恢复时,从挂起点继续。
2.3 co_return:结束协程
co_return expr;:返回expr并销毁协程帧。
- 语义:类似于return,但触发promise的return_value/return_void。
- 注意:协程不可用普通return。
2.4 协程函数的返回类型
协程不返回普通值,而是“句柄”(handle),如std::coroutine_handle<>。
- 自定义返回类型:需内嵌promise_type结构体。
Part 3: 协程框架与自定义类型
C++协程依赖“promise”和“awaitable”自定义点。标准库提供<std::coroutine>头文件,包括:
- std::coroutine_handle<Promise = void>
- std::suspend_always / std::suspend_never
3.1 Promise Type:协程的“大脑”
每个协程返回类型必须有嵌套的promise_type结构体。它控制协程生命周期。
promise_type成员:
- get_return_object():创建返回对象。
- initial_suspend():初始是否挂起(suspend_always/never)。
- final_suspend():结束是否挂起。
- return_void() / return_value(T):处理co_return。
- yield_value(T):处理co_yield。
- unhandled_exception():异常处理。
- await_transform(Expr):转换awaitable(可选)。
最小promise_type示例:
1
2
3
4
5
6
7
struct MyPromise {
auto get_return_object() { return MyCoroutine{this}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
3.2 Awaitable:可等待对象
Awaitable需实现三个方法(或通过operator co_await):
- await_ready():bool,是否立即就绪。
- await_suspend(std::coroutine_handle<> h):挂起逻辑,返回void/bool/handle。
- await_resume():恢复时返回值。
示例自定义Awaitable:
1
2
3
4
5
6
7
struct MyAwaitable {
bool await_ready() { return false; } // 总是挂起
void await_suspend(std::coroutine_handle<> h) {
// 异步操作完成后,h.resume();
}
int await_resume() { return 42; }
};
3.3 Coroutine Handle:控制协程
std::coroutine_handle
- resume():恢复协程。
- destroy():销毁帧。
- done():是否完成。
- promise():访问promise对象。
示例:
1
2
auto h = my_coroutine();
h.resume(); // 运行到第一个挂起
Part 4: 底层实现原理
C++协程的魔力在于编译器重写:将协程函数转换为状态机。
4.1 编译器转换过程
- 识别协程:函数含co_关键字。
- 分配协程帧:在堆上分配结构体,包含局部变量、参数、状态。
- 状态机生成:函数体拆分成switch-case,根据挂起点跳转。
- Promise集成:promise对象嵌入帧中。
伪代码表示: 原协程:
1
2
3
4
5
6
Coroutine foo(int x) {
int y = x + 1;
co_await something();
int z = y + 2;
co_return z;
}
编译后大致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Frame {
Promise promise;
int x, y, z;
int state = 0;
};
void foo_impl(Frame* frame) {
switch (frame->state) {
case 0:
frame->y = frame->x + 1;
frame->state = 1;
if (!something.await_ready()) {
something.await_suspend(handle_from_frame(frame));
return; // 挂起
}
// fallthrough if ready
case 1:
frame->z = frame->y + 2;
frame->promise.return_value(frame->z);
// 销毁
}
}
- 帧分配:operator new,通常在get_return_object中。
- 优化:编译器可elide堆分配如果生命周期短。
4.2 挂起与恢复机制
- 挂起:保存状态,返回控制权。
- 恢复:从handle.resume()跳转到switch的对应case。
这确保零开销:无额外栈,无虚拟函数调用(除自定义点)。
4.3 内存管理细节
-
协程帧生命周期:
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
创建调用点 ↓ [1] 分配协程帧(coroutine frame)——堆上 ↓ [2] 构造 promise_type 对象(嵌入帧中) ↓ [3] 调用 promise.get_return_object() → 得到外部可见的返回对象(如 Task<T>) ↓ [4] 调用 promise.initial_suspend() 并 co_await 它 ├─ 如果 await_ready() == true → 不挂起,继续执行协程体 └─ 如果 await_ready() == false → 挂起,返回到调用点(返回 return_object) ↓ [5] 协程体开始执行(从头或从上一个挂起点) ↓ ↻ 循环 [6] 遇到 co_await / co_yield → 调用对应 awaitable 的 await_suspend() ├─ await_suspend 返回 void → 挂起,控制权回到恢复者 ├─ 返回 bool → 返回 true 才挂起(false 则不挂起) └─ 返回 coroutine_handle → 立即 resume 另一个协程(链式调度) ↓ [7] 有人调用 handle.resume() → 从挂起点继续执行 ↓ ↻ [8] 遇到 co_return / 到达函数末尾 / 抛异常未捕获 ↓ [9] 调用 promise.unhandled_exception()(如果有异常) ↓ [10] 调用 promise.final_suspend() 并 co_await 它 ├─ 大多数实现返回 std::suspend_always{} → 故意挂起,不立即销毁帧 └─ 返回 std::suspend_never{} → 立即结束并销毁帧(危险!) ↓ [11] final_suspend 恢复后 → 销毁 promise 对象、局部变量、帧本身 (调用 coroutine_handle::destroy() 或 RAII 析构时自动销毁) -
避免泄漏:确保handle.destroy()调用。
-
移动语义:协程可移动,但不复制(unique)。
Part 5: 实际应用示例
5.1 生成器示例(C++20手动实现)
C++20无std::generator,需要自定义。
完整代码:
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <coroutine>
#include <iostream>
template <typename T>
struct Generator {
struct promise_type {
T value_;
Generator get_return_object() { return {this}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
std::suspend_always yield_value(T value) {
value_ = value;
return {};
}
void unhandled_exception() { std::terminate(); }
};
struct Iterator {
std::coroutine_handle<promise_type> h_;
Iterator& operator++() {
h_.resume();
return *this;
}
T operator*() { return &(h_.promise().value_); }
bool operator!=(const Iterator&) const { return !h_.done();; }
};
std::coroutine_handle<promise_type> h_;
Generator(promise_type* p) : h_(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~Generator() { if (h_) h_.destroy(); }
Iterator begin() { h_.resume(); return {h_; }
Iterator end() { return {nullptr, true}; }
};
Generator<int> fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
int main() {
for (int v : fib(10)) {
std::cout << v << " "; // 0 1 1 2 3 5 8 13 21 34
}
}
解释:
- promise_type处理yield_value,存储值在optional。
- Iterator模拟range-based for。
- 析构时destroy帧。
5.2 异步任务示例
假设简单定时器Awaitable。
代码:
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
#include <coroutine>
#include <iostream>
#include <chrono>
#include <thread>
struct TimerAwaitable {
std::chrono::milliseconds duration_;
TimerAwaitable(std::chrono::milliseconds d) : duration_(d) {}
bool await_ready() { return duration_.count() <= 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, d = duration_]() {
std::this_thread::sleep_for(d);
h.resume();
}).detach();
}
void await_resume() {}
};
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task async_main() {
std::cout << "Start\n";
co_await TimerAwaitable{std::chrono::seconds(2)};
std::cout << "After 2 seconds\n";
}
int main() {
async_main();
}
- TimerAwaitable:在suspend中启动线程定时resume。
5.3 协程链
“协程链”(coroutine chain)在协程语境中,通常指多个协程通过 co_await 相互连接,形成一个“等待链”或“恢复链”的现象。
它最核心的实现机制就是 symmetric transfer(对称转移),这是 C++20 协程设计中非常重要且优雅的一个特性,尤其在高性能异步库中被广泛使用。
下面从浅到深、从问题到解决方案完整讲清楚。
1. 为什么需要“协程链”?普通实现的局限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Task<int> bottom() {
co_return co_await read_from_network(); // 最底层操作
}
Task<int> middle() {
int v = co_await bottom();
co_return v * 10;
}
Task<int> top() {
int v = co_await middle();
co_return v + 1000;
}
Task<void> entry() {
int result = co_await top();
std::cout << result << "\n";
co_return;
}
假设我们实现了一个简单的 Task
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
40
41
42
#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() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> coro;
Task(std::coroutine_handle<promise_type> h) : coro(h) {}
~Task() {
if (coro) coro.destroy();
}
struct awaiter {
std::coroutine_handle<promise_type> coro;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
coro.resume(); // 直接 resume 子协程
}
void await_resume() {}
};
auto operator co_await() {
return awaiter{coro};
}
};
当链路启动后,resume 的流程大致是:
- 有人 resume entry()
- entry co_await top() → entry 挂起,resume top()
- top co_await middle() → top 挂起,resume middle()
- middle co_await bottom() → middle 挂起,resume bottom()
此时调用栈(逻辑上的调用栈)变成了:
1
2
3
4
5
6
7
entry.resume()
↓ call
top.resume()
↓ call
middle.resume()
↓ call
bottom.resume()
bottom 完成后:
- bottom → final_suspend → resume middle() → 栈变成:entry → top → middle → bottom.resume() → middle.resume()
- middle 完成 → resume top()
- top 完成 → resume entry()
关键问题: 每次从底层向上传播完成时,都会再压一层栈帧(因为 .resume() 是普通函数调用)。
如果链路有 N 层,最坏情况下栈深度可以达到 O(N),甚至在某些递归 await 场景下指数级增长。
当 N 很大(几千层)时 → 栈溢出崩溃。
2. 协程链的“对称转移”(symmetric transfer)解决方案
C++20 引入的关键机制:当 await_suspend() 返回 std::coroutine_handle<> 时,编译器会进行“对称转移”。
- 不是用 call 指令调用 .resume()
- 而是用 jmp(尾调用 / tail jump)指令直接跳转
- 当前协程的栈帧被“弹出”,直接跳到目标协程的 resume 点
- 栈深度不增长,恒定(通常 O(1) 栈空间)
这让协程链变成“扁平的”,不再是深调用栈,而是像事件循环里的“下一个任务”切换。
await_suspend 返回值的三种行为
| await_suspend 返回类型 | 行为描述 | 是否对称转移? | 栈增长? | 典型用途 |
|---|---|---|---|---|
| void | 挂起当前协程,控制权返回给 resume 调用者 | 否 | 否 | 普通异步 I/O、定时器 |
| bool | true=挂起,false=不挂起继续 | 否 | 否 | 快速路径短路 |
| std::coroutine_handle<> | 立即对称转移到返回的那个协程 | 是 | 否(尾跳) | 协程链、调度器 |
当返回 coroutine_handle 时,编译器生成的伪代码大致是:
1
2
3
4
5
6
7
8
9
// 原来(无对称转移)
awaiter.await_suspend(h_current);
return; // 返回给上层 resumer
// 有对称转移时(返回 handle)
auto next = awaiter.await_suspend(h_current);
// 编译器生成 tail call / jmp,而不是 call
// __builtin_tailcall 或直接 jmp 到 next.resume 的代码
next.resume(); // 但实际是 jmp,不 push 新栈帧
经典的“continuation 风格”实现协程链
大多数现代 C++ 协程库(如 folly::coro、cppcoro、asio 的 use_awaitable)都用这种模式:
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
struct TaskPromise {
std::coroutine_handle<> continuation; // 谁在等我完成?
auto initial_suspend() { return std::suspend_never{}; } // eager
auto final_suspend() noexcept {
struct Awaiter {
std::coroutine_handle<> cont;
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) noexcept {
return cont; // ← 这里返回 continuation → 对称转移!
}
void await_resume() noexcept {}
};
return Awaiter{continuation};
}
// ...
};
struct Task {
using promise_type = TaskPromise;
std::coroutine_handle<promise_type> h;
// 当别人 co_await 这个 Task 时,会调用这个 await_suspend
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) {
h.promise().continuation = caller; // 记住谁在等我
return h; // 立即转移到我自己(被 co_await 的那个协程)
}
// ...
};
Part 6: 调试与性能优化
6.1 调试技巧
- GDB/Clangd:支持协程栈帧查看。
- 日志:promise中log suspend/resume。
- 常见bug:忘记destroy导致泄漏;异常未处理。
6.2 性能优化
- 最小化挂起:用await_ready短路。
- 避免动态分配:自定义allocator。
- 基准工具:Google Benchmark。
Part 7: Other
7.1 异常处理
- unhandled_exception():默认terminate,可存储std::exception_ptr。
- 在await_resume中抛出:传播到调用者。
示例:
1
void unhandled_exception() { ep_ = std::current_exception(); }
恢复时检查ep_。
7.2 内存与性能
- 帧大小:sizeof(局部变量 + promise + state)。
- 优化:用std::suspend_never避免不必要挂起。
- 基准:协程切换 < 10ns vs 线程 ~1us。