C++20 协程是一种可暂停、可恢复 的函数机制。编译器会将协程转换为状态机,并通过 Coroutine Frame 保存执行上下文。Promise 负责管理协程生命周期和返回值,Handle 负责恢复和销毁协程,Awaiter 负责实现 co_await 的等待逻辑。协程属于用户态调度,切换成本远低于线程,但默认不能实现真正的并行执行。
一、核心概念
| 概念 | 定义 |
|---|---|
| 协程(Coroutine) | 可以暂停执行并在之后恢复的函数 |
| 协程帧(Coroutine Frame) | 堆上分配的内存,保存协程的局部变量、参数、执行位置等状态 |
| 有栈协程 | 每个协程独立栈空间(如 Go goroutine),切换需换栈指针 |
| 无栈协程(C++20采用) | 共享栈,通过状态机切换,仅修改几个状态变量,切换成本极低 |
| 协作式调度 | 协程主动让出执行权,非抢占式 |
二、三个关键字
| 关键字 | 含义 | 作用 |
|---|---|---|
co_await |
等待 | 暂停协程,等待某个可等待对象完成后恢复 |
co_yield |
产出/让出 | 产生一个值给调用者,然后挂起协程 |
co_return |
协程返回 | 结束协程,可带返回值 |
规则 :函数中包含任一关键字,即被识别为协程函数 ,返回类型必须包含
promise_type。
三、核心组件(三大件)
| 组件 | 作用 | 本质 |
|---|---|---|
| Promise Type | 控制协程生命周期和返回值 | 协程的"管家",定义在返回类型的嵌套类中 |
| Coroutine Handle | 恢复/销毁协程的句柄 | std::coroutine_handle<promise_type>,协程的"遥控器" |
| Awaiter | 定义 co_await 的等待行为 |
实现 await_ready()、await_suspend()、await_resume() 的对象 |
四、Promise Type 详解
4.1 必须实现的接口(5个)
| 函数 | 是否必须 | 调用时机 | 作用 |
|---|---|---|---|
get_return_object() |
✅ | 协程创建时 | 构造并返回协程对象给调用者 |
initial_suspend() |
✅ | 协程函数体执行前 | 控制协程是立即执行还是立即挂起 |
final_suspend() |
✅ | 协程函数体执行完毕后 | 控制协程结束时的行为 |
unhandled_exception() |
✅ | 协程内发生未捕获异常时 | 保存异常信息 |
return_void() / return_value(T) |
✅ 二选一 | 执行 co_return 时 |
处理返回值 |
4.2 可选接口
| 函数 | 作用 |
|---|---|
yield_value(T) |
使用 co_yield 时必须实现 |
operator new(size_t) |
自定义协程帧内存分配 |
operator delete(void*) |
自定义协程帧内存释放 |
4.3 各函数详解
get_return_object()
cpp
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
-
作用:给调用者返回一个"遥控器"(包含 handle 的对象)
-
关键 :用
from_promise(*this)将 promise 和 handle 关联
initial_suspend()
cpp
std::suspend_always initial_suspend() noexcept { return {}; } // 挂起(惰性启动)
std::suspend_never initial_suspend() noexcept { return {}; } // 不挂起(急切启动)
-
作用:决定协程创建后是立即执行还是先挂起
-
常用 :
suspend_always(惰性启动,最常用)
final_suspend()
cpp
std::suspend_always final_suspend() noexcept { return {}; } // 挂起(推荐)
std::suspend_never final_suspend() noexcept { return {}; } // 不挂起(危险)
-
作用:决定协程结束后是立即销毁还是先挂起
-
⚠️ 必须标记
noexcept -
推荐 :
suspend_always,让调用者读取完结果再手动destroy()
unhandled_exception()
cpp
void unhandled_exception() {
exception_ = std::current_exception(); // 保存异常
}
-
作用:捕获协程内未处理的异常
-
标准做法 :用
std::current_exception()保存,稍后rethrow_exception()重新抛出
return_void() / return_value(T)
cpp
// 无返回值协程:co_return;
void return_void() noexcept {}
// 有返回值协程:co_return 42;
void return_value(int value) {
value_ = value; // 保存到 promise 成员变量
}
-
二选一:不能同时存在
-
作用 :将
co_return的值保存到 promise 中,供外部读取
五、Coroutine Handle(协程句柄)
cpp
std::coroutine_handle<promise_type> handle;
| 方法 | 作用 |
|---|---|
handle.resume() |
恢复协程执行 |
handle.done() |
检查协程函数体是否执行完毕 |
handle.destroy() |
销毁协程帧,释放堆内存 |
handle.promise() |
获取关联的 promise 对象 |
注意 :
done() == true不代表协程帧已销毁,只是函数体执行完了。
六、Awaiter(可等待对象)
co_await 后面跟的对象必须实现三个接口:
| 接口 | 调用时机 | 返回值含义 |
|---|---|---|
bool await_ready() |
挂起前检查 | true = 不挂起直接继续;false = 需要挂起 |
void/bool/coroutine_handle await_suspend(handle) |
决定挂起时的操作 | void = 挂起; bool = true挂起/false不挂起; handle = 挂起当前并恢复目标 |
T await_resume() |
恢复后调用 | 返回值作为 co_await 表达式的结果 |
标准库提供的两个 Awaiter
cpp
std::suspend_always{}; // await_ready 始终返回 false(总是挂起)
std::suspend_never{}; // await_ready 始终返回 true(从不挂起)
七、协程生命周期(执行流程)
调用协程函数
│
▼
┌─────────────────┐
│ 1. 堆上分配协程帧 │ ← 编译器自动
│ 2. 构造 promise │
│ 3. get_return_object() │ ← 返回 Task 给调用者
└─────────────────┘
│
▼
┌─────────────────┐
│ 4. initial_suspend() │ ← 决定:挂起 or 继续
└─────────────────┘
│
├── 返回 suspend_always ──→ 协程挂起,控制权回 main()
│ │
│ ▼
│ main() 继续执行
│ │
│ ▼
│ 调用 task.resume()
│ │
│ ▼
│ 协程函数体开始/继续执行
│ │
│ 遇到 co_await
│ │
│ ┌─────────────┐
│ │ await_ready() │──→ true:不挂起,继续
│ └─────────────┘ false:挂起,回 main()
│ │
│ 再次 resume...
│ │
│ ▼
│ 遇到 co_return
│ │
│ ▼
│ return_value() / return_void()
│ │
│ ▼
│ final_suspend() ← 决定结束行为
│ │
│ ┌─────────────┐
│ │suspend_always│──→ 挂起,状态保留
│ │ (推荐) │ 需手动 destroy()
│ └─────────────┘
│ │
│ ┌─────────────┐
│ │suspend_never │──→ 协程帧立即销毁
│ │ (危险) │ 不能再访问数据
│ └─────────────┘
│
└── 返回 suspend_never ──→ 协程体立即执行
│
(后续流程同上)
八、co_yield 的本质
co_yield value 是语法糖,等价于:
cpp
co_await promise.yield_value(value);
执行流程:
-
调用
promise.yield_value(value)--- 将值保存到 promise -
yield_value返回一个 Awaiter(通常是suspend_always) -
执行
co_await这个 Awaiter --- 挂起协程
九、协程 vs 线程
| 对比项 | 线程 | C++20 协程 |
|---|---|---|
| 调度者 | 操作系统内核 | 用户程序 |
| 切换成本 | ~1-2μs(内核态切换) | ~10-20ns(用户态) |
| 内存占用 | 独立栈 1-8MB | 堆上按需分配,几十~几百字节 |
| 并发性 | 真正并行(多核同时执行) | 单线程内协作式,同一时刻只有一个执行 |
| 阻塞影响 | 阻塞线程浪费 OS 资源 | 阻塞只影响自己,线程可执行其他协程 |
| 适用场景 | CPU 密集型 | IO 密集型、高并发网络服务 |
十、C++20 协程的特点
| 特点 | 说明 |
|---|---|
| 编译器基础设施 | 标准只提供底层机制,没有现成 Task/Future |
| 无栈协程 | 只能在协程函数顶层挂起,不能在普通子函数内挂起 |
| 需要自行封装 | 实际项目通常使用 async_simple、cppcoro 等库 |
| 协程传染性 | 一旦使用协程,调用链上相关函数通常也需要改为协程风格 |
十一、常见陷阱
| 陷阱 | 后果 | 避免方法 |
|---|---|---|
忘记 destroy() |
内存泄漏 | 在 Task 析构函数中调用 handle.destroy() |
final_suspend 返回 suspend_never |
悬空引用,UB | 返回 suspend_always,让调用者控制销毁 |
协程内使用 this 指针后挂起 |
恢复时 this 可能悬空 |
使用 std::shared_ptr 或避免 |
| 未处理异常 | std::terminate |
在 unhandled_exception() 中保存异常 |
并发调用 resume() |
未定义行为 | 确保同一时刻只有一个线程 resume |
十二、最简完整代码模板
cpp
#include <iostream>
#include <coroutine>
// ========== 1. 定义返回类型(包含 promise_type)==========
struct Task {
struct promise_type {
// 返回值存储
int value_ = 0;
std::exception_ptr exception_;
// ① 返回对象
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 unhandled_exception() {
exception_ = std::current_exception();
}
// ⑤ 处理 co_return 值
void return_value(int v) {
value_ = v;
}
};
// 协程句柄(遥控器)
std::coroutine_handle<promise_type> handle;
// 便捷方法
void resume() { handle.resume(); }
bool done() { return handle.done(); }
int result() { return handle.promise().value_; }
// RAII 销毁
~Task() { if (handle) handle.destroy(); }
};
// ========== 2. 协程函数 ==========
Task my_coro() {
std::cout << "协程执行中..." << std::endl;
co_await std::suspend_always{}; // 挂起
std::cout << "协程恢复..." << std::endl;
co_return 42; // 返回值
}
// ========== 3. 使用 ==========
int main() {
auto task = my_coro(); // 创建,执行到 initial_suspend 挂起
task.resume(); // 执行到 co_await 挂起
task.resume(); // 执行到 final_suspend 挂起
std::cout << "结果: " << task.result() << std::endl; // 42
return 0; // task 析构,调用 destroy()
}
十三、记忆口诀
三关键字 :
co_await等一等,co_yield给一给,co_return回一回三大件:Promise 管生命周期,Handle 是遥控器,Awaiter 定义等待行为
五必写 :
get_return_object给遥控,initial_suspend定启动,final_suspend定结束,unhandled_exception抓异常,return_void/value存结果生命周期:创建 → 给遥控 → 定启动 → (挂起等 resume)→ 执行 → 定结束 → 手动销毁