前言:协程(Coroutine)是一种计算机程序组件,它与子例程(即通常所说的函数或过程)类似,但其执行方式更为灵活。不同于传统的线程和进程,协程允许在特定的地方暂停执行,并能在之后从暂停的地方恢复执行。这种特性使得协程在处理异步编程、并发操作以及I/O密集型任务时特别有用。
C++20 协程(Coroutines)学习指南
基于 cppreference.com、David Mazières(Stanford)教程、Simon Tatham 教程及知乎专栏整理
从概念到实践,逐层深入理解 C++20 无栈协程
目录
- 协程初探 —— 概念与动机
- 协程的核心三要素
- 第一个协程 —— 从零开始
- 协程的完整生命周期与控制流
- 深入 co_await —— 等待协议
- 承诺类型(Promise Type)完全解读
- 协程句柄(coroutine_handle)详解
- co_yield 与生成器模式
- co_return 与协程完成
- 对称转移(Symmetric Transfer)与协程链
- 实际应用模式
- 常见陷阱与最佳实践
第一章:协程初探 —— 概念与动机
1.1 什么是协程?
协程(Coroutine) 是一个可以暂停执行(suspend) 并在之后恢复执行(resume) 的函数。
普通函数一旦被调用,就从入口执行到出口,中间不可中断:
1 | int normal_function() { |
而协程可以在中间某个点暂停,将控制权交还给调用者,然后在之后的某个时刻从暂停点继续执行:
1 | task my_coroutine() { |
一句话总结:协程 = 可以暂停和恢复的函数。
1.2 协程 vs 线程
| 维度 | 协程(Coroutine) | 线程(Thread) |
|---|---|---|
| 调度方式 | 协作式(程序自己让出控制权) | 抢占式(操作系统强制切换) |
| 切换成本 | ≈ 函数调用(纳秒级) | 系统调用(微秒级,约 1000×) |
| 栈空间 | 无独立栈(C++20 无栈协程) | 有独立栈(通常 1-8 MB) |
| 并发模型 | 单线程内多个协程交替执行 | 多核并行执行 |
| 创建数量 | 数十万甚至百万 | 几千到数万 |
核心区别:协程是你可以暂停的函数,线程是你可以暂停的进程。协程的切换发生在用户态,完全由程序控制;线程的切换由操作系统内核完成,不可预测。
1.3 回调地狱 —— 协程要解决的问题
在异步编程中,传统做法是使用回调函数。当异步操作嵌套时,代码会急剧恶化:
1 | // 传统回调方式:回调地狱 |
使用协程后,同样的逻辑可以写成顺序代码:
1 | // C++20 协程方式:同步语法写异步代码 |
知乎文章指出,这正是 C++20 引入协程的核心动机——“以同步语法写异步代码”。
1.4 栈式协程 vs 无栈协程
| 对比维度 | 有栈协程(Stackful) | 无栈协程(Stackless) |
|---|---|---|
| 栈空间 | 堆上预分配独立栈(如 64KB) | 无独立栈,复用线程栈 |
| 栈溢出风险 | 有(需预分配足够空间) | 无 |
| 挂起深度 | 可嵌套挂起(任意深度函数调用) | 只能在协程体自身挂起 |
| 切换性能 | 需 swapcontext,较重 | ≈ 函数调用,纳秒级 |
| 是否 C++20 标准 | 否 | 是 |
| 代表实现 | Boost.Coroutine, libco | C++20 标准协程 |
C++20 选择了微软主导提出的无栈协程方案。无栈协程的本质是编译器将其转换为一个状态机,所有的局部变量被保存到堆分配的协程帧中。切换时不需要切换栈,因此成本极低。
知乎文章中的关键洞察:“无栈协程是普通函数的泛化”——普通函数只能一次性执行完毕,而无栈协程可以暂停和恢复,是对函数调用概念的推广。
1.5 C++20 协程的定位:构建套件
Simon Tatham 在他的文章中犀利地指出:C++20 没有提供开箱即用的协程——它提供的是构建套件(construction kit)。这意味着你必须自己实现大量样板代码:
- 定义承诺类型(
promise_type)来决定协程的行为策略 - 定义等待器类型(awaiter)来控制挂起/恢复的语义
- 定义返回对象类型来决定调用者如何与协程交互
如 Mazières 所言,C++20 协程就像"一堆垃圾下面的一个小金块",它灵活但学习曲线陡峭。
1.6 三个新关键字一览
| 关键字 | 用途 | 对应承诺方法 |
|---|---|---|
co_await expr |
暂停执行,等待异步操作完成 | await_transform |
co_yield expr |
向调用者返回一个值并暂停 | yield_value(expr) |
co_return expr |
结束协程并返回值 | return_value(expr) |
co_return; |
结束协程,无返回值 | return_void() |
函数体中只要包含以上任一关键字,该函数就是协程。
1.7 协程的限制
C++20 协程不能用于:
- 使用变长实参(variadic arguments)的函数
- 使用普通
return语句的函数 - 返回类型为
auto或 Concept 的函数 consteval/constexpr函数- 构造函数 / 析构函数
main函数
第二章:协程的核心三要素
每个 C++20 协程由三个核心对象协作完成:
1 | ┌──────────────────────────────────────────────────────┐ |
2.1 协程帧(Coroutine State / Coroutine Frame)
- 动态分配在堆上的存储区域(某些情况下编译器可优化为栈分配)
- 包含:形参副本、promise 对象、局部变量、暂停点信息
- 生命周期:从协程调用开始,到协程销毁结束
- 通过
coroutine_handle引用
2.2 承诺对象(Promise Object)
- 类型为
promise_type(嵌套在返回类型中) - 协程内部的控制器,决定协程的行为策略
- 负责:提交结果、处理异常、控制初始/最终挂起行为
- 不是
std::promise——两者除了名字之外毫无关系
2.3 协程句柄(Coroutine Handle)
- 类型为
std::coroutine_handle<Promise>,模板参数可省略 - 协程外部的操控器,用于:
resume()/operator()()—— 恢复协程执行destroy()—— 销毁协程帧done()—— 判断协程是否已执行完毕promise()—— 访问内部的 promise 对象
- 是非拥有句柄(类似指针),不负责生命周期
2.4 返回对象(Return Object)
- 调用者看到的返回值类型
- 由
promise.get_return_object()创建 - 通常持有
coroutine_handle以供调用者恢复协程 - 返回对象析构时可以选择是否
destroy()协程帧
第三章:第一个协程 —— 从零开始
本章基于 Mazières 教程和 Tatham 的最小示例,构建第一个完整的协程。
3.1 最简单的 promise_type
创建一个协程,需要三个要素:
- 返回类型:嵌套
promise_type的类 - 承诺类型:实现所需的接口方法
- 外部代码:通过
coroutine_handle控制协程
1 |
|
输出:
1 | counter: 开始执行 |
注意:使用
std::suspend_never作为initial_suspend和final_suspend的结果意味着协程既不延迟启动也不在结束时挂起——一切自动进行。
3.2 手动控制 —— 使用外部 Awaiter
要让调用者控制协程的暂停和恢复,需要自定义 awaiter:
1 |
|
输出:
1 | main: 恢复协程 |
执行流程:
1 | main() 调用 counter(h) |
3.3 编译要求
| 编译器 | 命令 |
|---|---|
| GCC | g++ -std=c++20 -fcoroutines file.cpp |
| Clang | clang++ -std=c++20 -stdlib=libc++ file.cpp |
| MSVC | cl /std:c++20 file.cpp(默认启用) |
注意:不同编译器对协程的支持成熟度不同。GCC 和 MSVC 对 C++20 协程支持最好,Clang 需要
-fcoroutines-ts标志并使用<experimental/coroutine>。
第四章:协程的完整生命周期与控制流
4.1 生命周期流程图
graph TD
A[调用协程函数] --> B[分配协程帧
operator new]
B --> C[复制参数到协程帧]
C --> D[构造 promise 对象]
D --> E[promise.get_return_object]
E --> F[promise.initial_suspend]
F --> G{是否立即挂起?}
G -->|suspend_always| H[挂起,返回 return_object]
G -->|suspend_never| I[继续执行协程体]
H --> J[调用者稍后 resume]
J --> I
I --> K{遇到什么操作?}
K -->|co_await expr| L[等待协议
见第5章流程图]
K -->|co_yield expr| M[yield_value
保存值并可能挂起]
K -->|co_return| N[return_void/return_value]
K -->|异常| O[unhandled_exception]
L --> I
M --> I
N --> P[逆序销毁局部变量]
O --> P
P --> Q[promise.final_suspend]
Q --> R{是否挂起?}
R -->|suspend_always| S[挂起,调用者需 destroy]
R -->|suspend_never| T[自动销毁]
S --> U[调用者调用 destroy]
U --> V[~promise / ~参数副本 / 释放帧]
T --> V
4.2 各阶段详解
阶段一:创建(Coroutine Creation)
当调用一个协程函数时,编译器生成的代码执行以下步骤:
- 分配协程帧:调用
operator new在堆上分配协程帧(可被优化掉) - 复制参数:将所有形参复制到协程帧中保存
- 构造 promise:在帧内构造
promise_type对象 - get_return_object():调用
promise.get_return_object()创建返回给调用者的对象 - initial_suspend():调用
promise.initial_suspend()并co_await其结果- 返回
suspend_always→ 协程挂起,控制权返回给调用者(惰性启动) - 返回
suspend_never→ 协程立即执行协程体(立即启动)
- 返回
参数生命周期陷阱:如果协程形参是引用类型,且引用对象在协程挂起后销毁,则恢复时引用悬垂。如:
1
2
3
4 task foo(const T& ref) { // 警告:按引用传递
co_await something; // 挂起点
ref.use(); // 危险!ref 可能已悬垂
}最佳实践:协程参数按值传递。
阶段二:执行(Coroutine Body Execution)
协程体正常执行,遇到以下情况之一会触发挂起或结束:
co_await expr→ 执行等待协议(详见第5章)co_yield expr→ 调用promise.yield_value(expr)并co_await结果co_return expr→ 调用promise.return_value(expr)co_return;→ 调用promise.return_void()- 函数末尾自然结束 → 调用
promise.return_void() - 异常抛出 → 调用
promise.unhandled_exception()处理异常
阶段三:挂起与恢复(Suspend & Resume)
当协程遇到 co_await 且等待器决定挂起时:
- 协程的当前执行状态(恢复点、寄存器)保存在协程帧中
- 控制权返回给"恢复者"(调用
resume()的那个代码) - 之后,任何代码(可在任意线程)调用
handle.resume()恢复执行 - 协程从挂起点之后继续执行
阶段四:完成(Coroutine Completion)
当协程执行完(co_return 或自然结束):
- 调用
promise.return_void()或promise.return_value(expr) - 按逆序销毁所有自动存储期局部变量
- 调用
promise.final_suspend()并co_await其结果- 返回
suspend_always→ 协程挂起,须由调用者显式destroy() - 返回
suspend_never→ 协程帧自动销毁
- 返回
重要:
final_suspend()返回suspend_always才可安全地在协程结束后读取 promise 中的数据。如果返回suspend_never,协程帧会立即销毁,访问 promise 是未定义行为。
阶段五:销毁(Coroutine Destruction)
promise对象析构- 形参副本析构
- 释放协程帧内存(
operator delete)
第五章:深入 co_await —— 等待协议
5.1 co_await 的完整处理流程
co_await expr 是协程中最核心的机制。编译器将其展开为一系列步骤:
graph TD
A["co_await expr"] --> B["initial/final_suspend
或 yield_value 上下文?"]
B -->|否| C["调用 promise.await_transform(expr)"]
B -->|是| D["expr 即为 awaitable"]
C --> E{"awaitable 是否有
operator co_await() ?"}
D --> E
E -->|有| F["调用 operator co_await()
得到 awaiter"]
E -->|无| G["awaitable 自身作为 awaiter"]
F --> H["调用 await_ready()"]
G --> H
H --> I{"结果已就绪?"}
I -->|true| J["不挂起
直接调用 await_resume()"]
I -->|false| K["挂起协程
保存恢复点"]
K --> L["调用 await_suspend(handle)"]
L --> M{"返回类型?"}
M -->|void| N["挂起,返回给调用者"]
M -->|bool| O{"true→挂起?
false→立即恢复?"}
O -->|false| P["不挂起
行为同 await_ready=true"]
O -->|true| N
M -->|coroutine_handle| Q["立即恢复指定协程
对称转移"]
N --> R["外部代码调用 resume()"]
R --> S["调用 await_resume()"]
P --> S
Q --> S
J --> T["co_await 表达式结果
= await_resume() 返回值"]
S --> T
5.2 两个转换步骤
第一步:expr → Awaitable
编译器将 co_await expr 中的 expr 转换为可等待体(awaitable):
- 如果该协程的 promise 类型定义了
await_transform()且不是 initial/final suspend 或 yield 的上下文:awaitable = promise.await_transform(expr)
- 否则:
awaitable = expr(直接使用表达式本身)
第二步:Awaitable → Awaiter
编译器将 awaitable 转换为等待器(awaiter):
- 如果 awaitable 有
operator co_await()成员(或自由函数):awaiter = awaitable.operator co_await()
- 否则:
awaiter = awaitable(awaitable 自身即为 awaiter)
5.3 等待器三成员函数
任何一个等待器(awaiter)必须实现以下三个函数:
await_ready()
1 | bool await_ready() const noexcept; |
- 返回
true:表示结果已就绪,不需要挂起,直接继续执行 - 返回
false:表示需要挂起协程,进入等待
这是高效短路机制——如果异步操作已完成(如数据已缓存),可以直接跳过挂起/恢复的开销。
await_suspend()
1 | // 三种合法的返回类型: |
接收的参数是当前协程的 coroutine_handle<>(类型擦除的句柄)。
重要安全提示:
await_suspend的参数h代表当前协程,当await_suspend被调用时,协程已经完全挂起。因此await_suspend可以将h发布到另一线程,那个线程可以调用h.resume()。但这也意味着当前协程可能在与await_suspend并发执行,所以await_suspend不应该在发布h后假设*this(awaiter 自身)仍然存活。
1 | // 危险示例:发布 handle 后访问 this |
await_resume()
1 | // 可以返回任意类型(包括 void) |
- 在协程恢复后调用
- 返回值即为
co_await表达式的值 - 如果抛出异常,异常会传播到
co_await表达式中
5.4 标准库工具
std::suspend_always —— 总是挂起
1 | struct suspend_always { |
std::suspend_never —— 从不挂起
1 | struct suspend_never { |
| 用途 | 常用类型 |
|---|---|
initial_suspend() 惰性启动 |
suspend_always |
initial_suspend() 立即启动 |
suspend_never |
final_suspend() 安全访问 promise |
suspend_always |
final_suspend() 自动销毁 |
suspend_never(谨慎使用) |
co_yield 挂起让调用者读取 |
suspend_always |
co_await 等待异步操作 |
自定义 awaiter |
5.5 完整示例:co_await 三种返回类型
1 |
|
第六章:承诺类型(Promise Type)完全解读
6.1 Promise Type 的查找机制
编译器使用 std::coroutine_traits<R, Args...> 来确定协程的 promise 类型:
- 默认实现:在返回类型
R中查找R::promise_type - 特化:可特化
std::coroutine_traits来使用外部 promise 类型
1 | // 返回类型结构: |
对于成员函数,coroutine_traits 会自动包含隐含的 this 指针类型:
1 | class MyClass { |
如果不想在返回类型中嵌套 promise_type,可以特化 coroutine_traits:
1 | // 外部 promise 类型定义 |
6.2 承诺接口详解
一个完整的 promise_type 需要实现以下接口:
get_return_object()
1 | ReturnType get_return_object(); |
- 创建返回给调用者的对象
- 通常在内部使用
coroutine_handle<promise_type>::from_promise(*this)获取句柄并存入返回对象
1 | struct promise_type { |
initial_suspend()
1 | // 返回一个 awaitable 对象 |
- 控制协程是否在创建后立即挂起
- 生成器通常返回
suspend_always(等待调用者首次resume) - 立即执行的任务通常返回
suspend_never
final_suspend()
1 | // 必须标记 noexcept!否则未定义行为 |
- 协程执行完毕(
co_return或异常处理后)调用 - 必须为
noexcept,否则行为未定义 - 返回
suspend_always:协程帧保持有效,调用者可在之后destroy() - 返回
suspend_never:协程帧立即自动销毁
如果需要从 promise 中读取数据(如生成器的
value_),final_suspend()必须返回suspend_always,否则协程帧在读取前就被销毁了。
return_void() vs return_value()
1 | // 二选一,不能同时存在(编译错误) |
co_return;→promise.return_void()co_return expr→promise.return_value(expr)- 协程体末尾自然结束 →
promise.return_void() - 如果定义了
return_value()但协程中写了co_return;则编译错误,反之亦然
yield_value()
1 | // 等价于 co_await promise.yield_value(expr) |
co_yield expr编译器展开为co_await promise.yield_value(expr)- 返回的 awaitable 控制是否在 yield 后挂起
- 通常用于生成器模式
unhandled_exception()
1 | void unhandled_exception() { |
- 当协程体内抛出未被捕获的异常时调用
- 调用后,编译器会自动调用
final_suspend() - 通常保存异常指针,在之后读取结果时重新抛出
6.3 完整的 promise 示例
1 | template<typename T> |
第七章:协程句柄(coroutine_handle)详解
7.1 什么是 coroutine_handle?
std::coroutine_handle<Promise> 是一个非拥有句柄,类似于一个指针,指向协程帧。它是协程外部世界的操控接口。
- 定义在
<coroutine>头文件中 - 是
LiteralType(可在编译期构造) - 通常是
TriviallyCopyable(可简单拷贝) - 析构函数不会销毁协程帧——需要显式调用
destroy()
7.2 基本操作
1 |
|
7.3 类型体系
1 | // 类型擦除版本:coroutine_handle<void> |
7.4 noop_coroutine
std::noop_coroutine() 是一个特殊的协程句柄,表示一个"无操作"协程:
1 |
|
- 类型为
std::noop_coroutine_handle(是coroutine_handle<noop_coroutine_promise>的别名) - 可转换为
coroutine_handle<> - 用于某些需要返回协程句柄但实际不需要操作的场景
7.5 生命周期管理
coroutine_handle 是一个非拥有句柄——它不自动管理协程帧的生命周期:
1 | // ⚠️ 错误示例:忘记 destroy() |
7.6 安全指南
| 注意点 | 说明 |
|---|---|
| 悬垂句柄 | 协程帧已销毁后继续使用 resume() 是未定义行为 |
| double destroy | 对同一帧调用两次 destroy() 是未定义行为 |
| done() 的时机 | done() 只在协程挂起到 final_suspend 后返回 true,其他情况可能不准确 |
| 线程安全 | resume() / destroy() 在同一时刻只能由一个线程调用,否则数据竞争 |
| promise 访问 | final_suspend 返回 suspend_never 后访问 promise() 是未定义行为 |
1 | // 安全使用模式 |
第八章:co_yield 与生成器模式
8.1 co_yield 的本质
co_yield expr 是 C++20 协程中用于向调用者生成一系列值的机制。编译器将其展开为:
1 | co_yield expr; |
因此,yield_value() 返回的 awaitable 决定了 yield 之后的控制流——通常是 suspend_always 以挂起协程,让调用者读取值。
8.2 使用 promise.value_ 传递数据
先看一个使用 yield_value 在 promise 中存储值的示例(对应 Mazières 的 counter4):
1 |
|
输出:
1 | counter4: 0 |
8.3 构建泛型生成器 Generator
基于 Mazières 教程的最终示例,构建一个通用的生成器:
1 |
|
输出:
1 | Fibonacci: 0 1 1 2 3 5 8 13 21 34 |
8.4 生成器调用时序图
sequenceDiagram
participant Caller as 调用者 (main)
participant Coro as 协程帧 (counter)
participant Promise as promise_type
participant Awaiter as suspend_always
Note over Caller,Awaiter: 创建(惰性)
Caller->>Coro: auto gen = counter()
Coro->>Coro: 分配帧、构造 promise
Coro->>Promise: get_return_object()
Promise-->>Caller: 返回 Generator 对象
Coro->>Promise: initial_suspend() -> suspend_always
Note over Coro: 挂起(尚未执行循环体)
Note over Caller,Awaiter: 第 1 次恢复
Caller->>Coro: gen() 或 bool(gen)
Coro->>Coro: 进入循环 i=0
Coro->>Promise: co_yield 0 -> yield_value(0)
Promise->>Promise: value_ = 0
Promise->>Awaiter: return suspend_always{}
Awaiter-->>Caller: 挂起,返回给调用者
Caller->>Promise: 读取 value_ -> 0
Note over Caller,Awaiter: 第 2 次恢复
Caller->>Coro: gen()
Coro->>Coro: 继续循环 i=1
Coro->>Promise: co_yield 1 -> yield_value(1)
Promise->>Promise: value_ = 1
Promise->>Awaiter: return suspend_always{}
Awaiter-->>Caller: 挂起
Caller->>Promise: 读取 value_ -> 1
Note over Caller,Awaiter: 第 3 次恢复(结束)
Caller->>Coro: gen()
Coro->>Coro: i=2, co_yield 2
Coro->>Coro: i=3, 循环结束
Coro->>Promise: co_return 调用 return_void()
Coro->>Coro: 逆序销毁局部变量
Coro->>Promise: final_suspend() -- 返回 suspend_always
Note over Coro: 挂起(done() 为 true)
Caller->>Coro: !gen -> 循环结束
Caller->>Coro: gen 析构 -> h_.destroy()
8.5 C++23 std::generator
C++23 标准库引入了 std::generator<T>,提供了开箱即用的生成器:
1 |
|
第九章:co_return 与协程完成
9.1 co_return 的两种形式
1 | // 形式1:无返回值 |
return_void()和return_value()不能同时存在(编译错误)。
9.2 完整示例
1 |
|
9.3 final_suspend 的重要性
final_suspend() 的返回值选择至关重要:
| 返回值 | 后果 |
|---|---|
suspend_always |
协程帧保持有效,调用者可读取 promise、检查 done()、配合 RAII 自动 destroy() ✅ |
suspend_never |
协程帧立即销毁,访问 promise = 未定义行为 ❌ |
1 | // ❌ 错误:final_suspend 返回 suspend_never |
9.4 done() 的正确使用
std::coroutine_handle::done() 返回协程是否已完成(即挂起在 final_suspend 之后):
1 | auto h = counter5(); |
重要:
done()只在协程挂起到final_suspend时才返回true。如果协程从未开始执行或挂起在普通点,done()返回false。
第十章:对称转移(Symmetric Transfer)与协程链
10.1 什么是对称转移?
当 await_suspend 返回一个 coroutine_handle 时,控制权立即转移到那个协程,而不是返回给调用者。
1 | 不对称(Asymmetric):调用者 resume → 协程A → 挂起 → 返回调用者 |
优点:
- 避免深度嵌套的
resume调用导致的栈溢出 - 协程之间可以直接传递控制权,无需中间人
- 适合实现协程调度器、管道(pipeline)
10.2 为什么需要对称转移?
考虑一个简单的管道场景:协程 A 生成数据,协程 B 处理数据。
如果使用不对称转移:
1 | // 调用者负责调度 |
如果 A 和 B 之间相互传递控制权,而不经过调用者,就用到了对称转移:
1 | // await_suspend 返回 coroutine_handle<> |
10.3 对称转移示例
1 |
|
输出:
1 | main: 恢复 A,启动交替执行 |
10.4 调用栈分析
不对称转移的调用栈:
1 | frame 0: main → resume(hA) |
对称转移的调用栈:
1 | frame 0: main → resume(hA) |
表面上看对称转移增加了一个栈帧,但注意:对称转移避免了不断的 resume/return 循环。更重要的是,在复杂的调度场景中(如协程链 A → B → C → D → …),不对称转移会导致调用者的调度函数深度嵌套,而对称转移让协程之间直接传递控制权。
注意:在某些实现中,返回
coroutine_handle的await_suspend可能被编译器优化为 tail call,从而避免栈的增长。
第十一章:实际应用模式
模式一:惰性生成器
已在第8章完整实现 Generator<T>。核心特征:
initial_suspend()返回suspend_always(惰性)final_suspend()返回suspend_always(安全访问值)yield_value()在 promise 中保存值并挂起- RAII 封装自动管理
destroy()
模式二:跨线程切换
基于知乎文章中的跨线程示例,实现一个从当前线程切换到新线程的 awaiter:
1 |
|
可能的输出:
1 | 协程在 线程 12345 开始执行 |
跨线程协程调用时序图
sequenceDiagram
participant Main as main() 线程
participant Coro as 协程帧
participant Promise as promise_type
participant Awaiter as 线程切换 Awaiter
participant Thread as 新线程
Note over Main,Thread: 创建阶段
Main->>Coro: 调用协程函数
Coro->>Coro: 分配帧
Coro->>Promise: 构造 promise
Promise->>Promise: get_return_object()
Promise->>Coro: initial_suspend() → suspend_never
Note over Coro: 协程开始执行
打印 "线程 A"
Note over Main,Thread: 挂起与转移
Coro->>Awaiter: co_await SwitchToNewThread(worker)
Awaiter->>Awaiter: await_ready() → false
Awaiter->>Awaiter: 保存 handle
Awaiter->>Thread: std::jthread(lambda -> handle.resume())
Note over Awaiter: ⚠ 发布 handle 后
this 可能已销毁
Awaiter-->>Main: 返回给调用者
Note over Main,Thread: 恢复阶段
Thread->>Coro: handle.resume()
Note over Coro: 协程在新线程恢复
打印 "线程 B"
Coro->>Coro: co_return;
Coro->>Promise: return_void()
Coro->>Coro: 逆序销毁局部变量
Coro->>Promise: final_suspend() → suspend_always
Note over Coro: 挂起(done() 为 true)
Note over Coro: 自动销毁
模式三:await_transform 实现优先级调度
基于 Josuttis 的优先级调度示例,展示 await_transform 的威力:
1 |
|
第十二章:常见陷阱与最佳实践
12.1 悬垂引用(Dangling Reference)
按引用传递参数是协程中最常见的陷阱:
1 | // ❌ 危险:按引用传递 |
规则:协程参数始终按值传递。如果必须按引用传递,确保引用对象的生命周期超过协程的整个生命周期。
12.2 句柄生命周期管理
1 | // ❌ 错误:忘记 destroy |
12.3 final_suspend 返回 suspend_never 的风险
1 | // ❌ 危险:final_suspend 返回 suspend_never |
规则:除非你完全确定协程结束后不再需要访问 promise 或句柄,否则 final_suspend 永远返回 suspend_always。
12.4 异常安全
1 |
|
12.5 内存分配优化
C++20 标准允许编译器在某些条件下优化掉协程帧的堆分配:
1 | // 条件: |
此外,可以自定义协程帧的分配器:
1 | struct MyPromise { |
12.6 协程是"库作者工具"
如知乎文章所强调的,C++20 协程的设计面向的是库作者而非普通开发者。日常使用中:
| 你的角色 | 建议 |
|---|---|
| 应用开发者 | 使用现成的协程库(如 async_simple、cppcoro、Folly::coro) |
| 库作者 | 学习本文档,实现自己的 promise_type 和 awaiter 类型 |
| 学习者 | 从生成器开始,逐步深入协程调度器 |
12.7 快速检查清单
- [ ] 参数按值传递(避免悬垂引用)
- [ ]
final_suspend()标记为noexcept - [ ] 使用 RAII 管理
destroy()(或明确谁负责销毁) - [ ]
final_suspend()返回suspend_always(除非有特殊理由) - [ ] 禁止拷贝返回对象(避免 double destroy)
- [ ]
await_suspend中发布 handle 后不再访问*this - [ ] 跨线程时确保
resume()/destroy()不会并发执行 - [ ]
return_void()和return_value()不共存
附录
A. 标准库 头文件一览
| 组件 | 说明 |
|---|---|
std::coroutine_traits<R, Args...> |
协程特征,确定 promise_type |
std::coroutine_handle<Promise> |
协程句柄模板 |
std::coroutine_handle<> |
类型擦除的协程句柄 |
std::noop_coroutine_promise |
无操作协程承诺类型 |
std::noop_coroutine_handle |
coroutine_handle<noop_coroutine_promise> 的别名 |
std::noop_coroutine() |
创建无操作协程句柄 |
std::suspend_never |
从不暂停的可等待体 |
std::suspend_always |
总是暂停的可等待体 |
B. 各资料特色对比
| 资料源 | 特色 | 适合读者 |
|---|---|---|
| cppreference.com | 权威准确,接口完整 | 作为参考手册查阅 |
| Mazières 教程 | 渐进示例,逐步构建 | 想理解底层机制的初学者 |
| Simon Tatham 教程 | 系统设计视角,三种类型分析 | 想写自定义协程系统的开发者 |
| 知乎文章 | 中文深度解读,有栈/无栈辨析 | 中文读者,快速建立认知框架 |
| Lewis Baker 系列 | 最深入的技术分析 | 进阶/专家级开发者 |
C. 关键术语中英文对照
| 英文 | 中文 | 说明 |
|---|---|---|
| coroutine | 协程 | 可挂起/恢复的函数 |
| stackless coroutine | 无栈协程 | C++20 标准采用的方案 |
| coroutine frame / state | 协程帧 / 协程状态 | 堆上分配的存储区域 |
| promise object | 承诺对象 | 协程内部控制器 |
| promise_type | 承诺类型 | 必须嵌套在返回类型中的类型 |
| coroutine handle | 协程句柄 | 外部操控协程的句柄 |
| return object | 返回对象 | 调用者看到的返回值类型 |
| awaitable | 可等待体 | 可用作 co_await 操作数的类型 |
| awaiter | 等待器 | 实现了 await_ready/suspend/resume 的类型 |
| awaiter protocol | 等待协议 | co_await 的编译器展开规则 |
| await_transform | 等待转换 | promise 中转换 co_await 操作数的方法 |
| suspend | 挂起 | 暂停协程执行 |
| resume | 恢复 | 继续协程执行 |
| symmetric transfer | 对称转移 | 协程之间直接传递控制权 |
| asymmetric transfer | 不对称转移 | 协程挂起后返回给恢复者 |
| coroutine_traits | 协程特征 | 确定 promise_type 的元函数 |
D. 推荐阅读
- cppreference.com: Coroutines (C++20)
- David Mazières: My tutorial and take on C++20 coroutines
- Simon Tatham: Writing custom C++20 coroutine systems
- 知乎专栏: C++20 协程原理和应用
- Lewis Baker: Asymmetric Transfer — 最深入的 C++20 协程技术分析系列
- Alibaba async_simple: https://github.com/alibaba/async_simple
- Nicolai Josuttis: C++20 - The Complete Guide — 包含完整的协程调度器示例
文档完成日期:2026-07-09
参考资料版本:基于 C20 标准(最终草案),部分提及 C23 特性
反馈与改进:如有发现错误或遗漏,欢迎指正