本文基于 cppreference.com – Coroutines (C++20) 的内容整理扩展而成。
1. 协程介绍
1.1 什么是协程
协程(Coroutine) 是一种可以在执行过程中暂停并恢复的函数。与普通函数不同,协程不是一次性运行到结束,而是可以在某个点“挂起”(suspend),将控制权交还给调用者;之后再从挂起点继续执行。
特性概述:
- 栈无关:协程不依赖于调用栈来保存状态。当它挂起时,其局部变量和执行位置被保存在一个独立分配的“协程状态对象”中。这使得协程的状态可以长期存在,甚至跨线程传递。
- 支持异步编程模型:通过
co_await关键字,开发者可以用同步的方式编写异步代码,显著降低复杂度,避免回调嵌套带来的“回调地狱”。 - 适用于生成器模式:利用
co_yield可以轻松实现一个按需生成数据的序列,如无限数列或流式数据处理。 - 支持语言级原语:C++20 将
co_await、co_yield、co_return作为关键字引入,编译器会根据这些关键字自动生成相应的协程控制逻辑。
示例
// 示例:一个简单的异步 TCP 回显服务器
task<> tcp_echo_server() {
char data[1024];
while (true) {
std::size_t n = co_await socket.async_read_some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
这段代码看起来像是同步阻塞风格,但实际上每个 co_await 都可能使协程挂起,直到 I/O 操作完成后再恢复执行。整个过程不会阻塞线程,实现了高效的非阻塞通信。
1.2 如何定义协程
一个函数要成为协程,必须在其函数体内使用以下三个关键字之一:
| 关键字 | 含义 |
|---|---|
co_await |
暂停执行,等待某个操作完成,常用于异步任务 |
co_yield |
返回一个值并暂停,用于生成器(generator)模式 |
co_return |
结束协程并返回结果 |
只要出现其中任意一个,该函数就被视为协程。
后文将对这三个关键字进行阐述。
示例:
// 是协程 — 使用了 co_yield
// generator<T> 用于惰性生成序列
generator<int> iota(int start = 0) {
while (true)
co_yield start++;
}
// 是协程 — 使用了 co_await
// task<T> 用于异步操作
task<void> fetch_data() {
auto result = co_await http_client.get("/api/data");
process(result);
}
// 是协程 — 使用了 co_return
// lazy<T> 延迟计算结果
lazy<int> compute() {
co_return 42;
}
// 不是协程 — 虽然返回类型是 task,但没有使用任何协程关键字
task<void> not_a_coroutine() {
return;
}
注意:可以看到返回类型比较特殊,但并不与协程挂钩,仅靠返回类型无法判断是否为协程!必须有
co_await/co_yield/co_return才能触发编译器生成协程逻辑。
1.3 协程的限制
由于协程涉及复杂的生命周期管理和状态转移,C++ 对其施加了一些限制:
禁止使用的特性及其原因:
| 特性 | 原因说明 |
|---|---|
可变参数(...) |
参数复制和移动过程在协程状态下难以统一处理,容易引发未定义行为 |
普通 return 语句 |
协程只能通过 co_return 正确结束,普通 return 无法参与协程状态的清理流程 |
自动类型推导返回值(auto 或 concept) |
编译器需要提前知道返回类型的 promise_type,否则无法构造协程框架 |
constexpr / consteval 函数 |
当前标准不支持在编译期求值协程逻辑 |
| 构造函数、析构函数 | 这些函数具有特殊的生命周期语义,与协程的延迟执行机制冲突 |
main 函数 |
标准明确规定 main 不能是协程,因为它是程序入口点,必须立即执行完毕 |
示例错误:
auto f() { co_return 1; } // 错误:不能使用 auto 推导协程返回类型
2. 协程的执行模型
每个协程背后都关联着一组内部结构,共同构成其执行环境。
2.1 协程的核心组件
| 组件 | 描述 |
|---|---|
| Promise 对象 | 存储协程的结果或异常,由协程内部访问(类似 promise/future 中的 promise) |
| Coroutine Handle(协程句柄) | 外部控制协程的非拥有式句柄,可用于 resume 或 destroy |
| Coroutine State(协程状态) | 动态分配的对象,包含: • Promise 对象 • 参数副本 • 局部变量和临时对象(跨越挂起点) • 当前挂起点信息 |
这些组件共同构成协程的“运行上下文”,通常在堆上分配,除非被优化掉。
2.2 协程启动流程
当协程首次被调用时,编译器会自动生成一系列初始化步骤:
- 分配协程状态对象
- 使用
operator new在堆上分配足够大的内存块,容纳所有必要数据。 - 分配大小取决于
sizeof(Promise)+ 参数总大小 + 跨越挂起点的局部变量大小。
- 使用
- 复制参数
- 值传递的参数会被拷贝或移动到协程状态中。
- 引用参数仍保持为引用——这意味着如果所引用的对象在其生命周期结束后协程才恢复,则会导致悬空引用(dangling reference),属于未定义行为。
- 构造 Promise 对象
- 如果
PromiseType提供了一个接受所有协程参数的构造函数,则调用该构造函数。 - 否则调用默认构造函数。
- 如果
- 调用
promise.get_return_object()- 获取一个代表协程的返回对象(如
task<T>或generator<T>)。 - 该对象会立即返回给调用者,即使协程尚未真正开始运行。
- 获取一个代表协程的返回对象(如
- 调用
promise.initial_suspend()并co_await其结果- 决定协程是否应在启动后立即挂起:
- 返回
std::suspend_always{}→ 协程挂起,等待手动 resume - 返回
std::suspend_never{}→ 协程立即进入函数体执行
- 返回
- 这一步决定了协程是“懒启动”还是“急启动”。
- 决定协程是否应在启动后立即挂起:
- 进入协程体执行
- 开始执行用户编写的函数逻辑。
所有这些步骤中的异常都会直接传播回调用者,而不是封装进 Promise 对象。
2.3 悬挂点
协程可以在多个位置暂停执行,称为“悬挂点”。常见的悬挂点包括:
co_await exprco_yield exprco_return expr- 函数入口(由
initial_suspend控制) - 函数末尾(由
final_suspend控制)
每次挂起时,当前局部变量和执行位置都会保留在协程状态中。
3. 协程的终止
协程可以通过正常返回或异常终止两种方式结束。
3.1 正常结束:co_return
当协程执行到 co_return 语句时,按照以下顺序进行清理:
- 调用对应的 Promise 方法
- 若无返回值或返回 void 类型表达式,调用
promise.return_void() - 若有非 void 表达式,调用
promise.return_value(expr)
- 若无返回值或返回 void 类型表达式,调用
- 销毁局部变量
- 按照声明的逆序销毁所有具有自动存储期的局部变量。
- 调用
promise.final_suspend()并co_await其结果- 通常用于通知外部协程已完成,或触发后续操作(如唤醒 continuation)。
- 与
initial_suspend类似,可返回suspend_always或suspend_never。
- 释放协程状态
- 调用
operator delete释放堆上的协程状态内存。
- 调用
co_return 的语义映射
co_return 形式 |
调用的 Promise 方法 |
|---|---|
co_return; |
promise.return_void() |
co_return expr;(expr 类型为 void) |
promise.return_void() |
co_return expr;(expr 非 void) |
promise.return_value(expr) |
注意事项
若函数体自然“掉出结尾”而没有显式的 co_return,其效果等同于 co_return;。但如果 Promise 类型未定义 return_void(),则程序为 ill-formed(不符合语法规范)。这是 CWG 2556 缺陷报告修复的内容。
task<void> f() {
// 错误!没有 co_return,也没有 co_await
// 如果 Promise 没有 return_void,程序将无法通过编译
}
3.2 异常终止
如果协程抛出了未被捕获的异常,处理流程如下:
- 在协程内部捕获异常。
- 调用
promise.unhandled_exception(),通常在此方法中保存异常指针(std::current_exception())。 - 调用
promise.final_suspend()并co_await其结果。 - 禁止从此点恢复协程,否则行为未定义。
这保证了即使发生异常,协程也能安全地完成清理工作。
4. 动态内存分配与优化
4.1 默认行为
默认情况下,协程状态通过全局 operator new(std::size_t) 在堆上动态分配。释放则使用匹配的 operator delete。
分配大小由编译器计算,至少包括:
- Promise 对象
- 所有按值传递的参数
- 所有跨越挂起点的局部变量和临时对象
4.2 自定义分配器
可以通过在 PromiseType 中重载 operator new 和 operator delete 来指定自定义内存管理策略:
struct MyPromise {
static void* operator new(std::size_t n) {
return std::malloc(n);
}
static void operator delete(void* p, std::size_t n) {
std::free(p);
}
};
更进一步,支持“前置分配器约定”(leading allocator convention):
void* operator new(std::size_t n, MyAllocator& alloc, int extra_arg);
只要签名匹配,编译器会在调用 new 时自动传入请求大小及协程参数,便于实现上下文感知的内存池。
4.3 分配失败处理
如果 operator new 抛出异常,默认行为是传播 std::bad_alloc。
但可通过定义静态成员函数 get_return_object_on_allocation_failure() 来提供回退机制:
struct MyPromise {
static MyTask get_return_object_on_allocation_failure() {
return MyTask(nullptr); // 返回无效 task
}
void* operator new(std::size_t n) noexcept {
return malloc(n); // 返回 nullptr 表示失败
}
};
此时使用 nothrow 版本的 new,失败时不抛异常,而是直接返回 fallback 对象。
4.4 分配优化:栈内嵌入
在满足以下两个条件时,协程状态可被优化至调用者的栈帧中(无需堆分配):
- 协程的生命周期严格嵌套于调用者之内(即不会逃逸)
- 协程帧的大小在调用点已知
例如:
void caller() {
auto gen = fibonacci_sequence(10); // 可能内联到 caller 栈上
for (auto v : gen) { /* ... */ }
}
这种优化极大地提升了性能,减少了内存分配开销和缓存 misses。
5. Promise 类型详解
5.1 Promise 的作用
Promise 是协程机制的核心纽带,负责协调协程内外的行为。其主要职责包括:
- 创建返回对象(
get_return_object) - 控制初始挂起行为(
initial_suspend) - 控制最终挂起行为(
final_suspend) - 处理返回值(
return_value/return_void) - 处理未捕获异常(
unhandled_exception) - 自定义
co_await行为(await_transform)
每个协程都有一个与之关联的 Promise 实例,位于协程状态内部。
5.2 Promise 类型的确定规则
Promise 类型由模板 std::coroutine_traits<R, Args...>::promise_type 决定,具体规则如下:
- 对于普通函数:
std::coroutine_traits<R, Args...>::promise_type - 对于非静态成员函数:
- 若为左值限定(非 rvalue-reference-qualified),则为
std::coroutine_traits<R, cv ClassT&, Args...>::promise_type - 若为右值限定,则为
std::coroutine_traits<R, cv ClassT&&, Args...>::promise_type
- 若为左值限定(非 rvalue-reference-qualified),则为
示例对照表
| 协程定义 | 对应的 Promise 类型 |
|---|---|
task<void> foo(int x); |
std::coroutine_traits<task<void>, int>::promise_type |
task<void> Bar::foo(int x) const; |
std::coroutine_traits<task<void>, const Bar&, int>::promise_type |
task<void> Bar::foo(int x) &&; |
std::coroutine_traits<task<void>, Bar&&, int>::promise_type |
用户必须确保返回类型(如
task<T>)提供了正确的嵌套*promise_type,否则编译失败。
6. co_await 表达式详解
co_await expr 是协程暂停与恢复的核心机制,其实现依赖于 Awaitable 和 Awaiter 协议。
名词 含义 Awaitable 任何可以出现在 co_await后面的表达式所对应的类型。例如:std::suspend_always、自定义任务类等。Awaiter 实际执行挂起逻辑的对象。它是从 Awaitable 转换而来的,必须提供三个特定成员函数: await_ready、await_suspend、await_resume。
6.1 执行流程
- 转换为 Awaitable
- 若在初始/最终挂起或 yield 上下文中,直接使用
expr - 否则尝试调用
promise.await_transform(expr) - 否则使用
expr本身
- 若在初始/最终挂起或 yield 上下文中,直接使用
- 获取 Awaiter 对象
- 查找
operator co_await- 成员函数:
expr.operator co_await() - 非成员函数:
operator co_await(expr)
- 成员函数:
- 若未找到,则 awaiter 就是 awaitable 本身
- 若重载歧义 → 编译错误
- 查找
- 判断是否需要挂起
- 调用
awaiter.await_ready() - 返回
false表示需要挂起
- 调用
- 执行挂起
- 调用
awaiter.await_suspend(handle) - 参数为当前协程的
coroutine_handle - 返回类型决定后续动作:
void:永久挂起,控制权交还调用者bool:true挂起,false继续执行coroutine_handle:立即 resume 另一个协程(可用于链式调度)
- 调用
- 恢复后调用
await_resume()- 无论是否真正挂起,都会调用此函数
- 返回值即为
co_await expr的求值结果
恢复点位于
await_resume()调用之前。
6.2 标准 Awaitables
C++20 提供两个简单 awaitable:
| 类型 | 行为 |
|---|---|
std::suspend_always |
总是挂起 |
std::suspend_never |
从不挂起 |
它们的作用是为协程提供最基础、最常用的挂起控制行为。它们被设计得非常轻量且语义清晰,主要用于实现协程的 初始挂起(initial_suspend) 和 最终挂起(final_suspend) 行为。
std::suspend_always{}; // 常用于 initial_suspend 或 final_suspend
std::suspend_never{}; // 让协程立即运行
示例:切换线程的 Awaiter
auto switch_to_new_thread(std::jthread& out) {
struct awaitable {
std::jthread* p_out;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::jthread& out = *p_out;
if (out.joinable())
throw std::runtime_error("Already joined");
out = std::jthread([h] { h.resume(); });
std::cout << "New thread ID: " << out.get_id() << '\n';
}
void await_resume() {}
};
return awaitable{&out};
}
task resuming_on_new_thread(std::jthread& out) {
std::cout << "Start on: " << std::this_thread::get_id() << '\n';
co_await switch_to_new_thread(out);
std::cout << "Resume on: " << std::this_thread::get_id() << '\n';
}
输出可能为:
Start on: 139972277602112
New thread ID: 139972267284224
Resume on: 139972267284224
注意:
await_suspend中不要访问*this,因为 awaiter 可能在其他线程被销毁。
6.3 await_transform:自定义 await 行为
PromiseType 可以定义 await_transform 来拦截所有 co_await 表达式,实现统一包装或日志等功能。
示例:可控挂起的协程
struct tunable_coro {
class tunable_awaiter {
bool ready_;
public:
explicit(false) tunable_awaiter(bool ready) : ready_(ready) {}
bool await_ready() const noexcept { return ready_; }
static void await_suspend(std::coroutine_handle<>) noexcept {}
static void await_resume() noexcept {}
};
struct promise_type {
bool ready_{true};
auto get_return_object() { /* ... */ }
static auto initial_suspend() { return std::suspend_always(); }
static auto final_suspend() noexcept { return std::suspend_always(); }
static void return_void() {}
static void unhandled_exception() { std::terminate(); }
// 拦截所有 co_await
auto await_transform(std::suspend_always) {
return tunable_awaiter(!ready_);
}
void disable_suspension() { ready_ = false; }
};
// ...
};
tunable_coro generate(int n) {
for (int i = 0; i < n; ++i) {
std::cout << i << ' ';
co_await std::suspend_always{}; // 实际变为 tunable_awaiter
}
}
调用示例:
auto coro = generate(8);
coro(); // 输出 0
coro(); // 输出 1
// ...
coro.disable_suspension(); // 关闭挂起
coro(); // 输出剩余全部数字(5 6 7)
输出:
0 1 : 2 : 3 : 4 : 5 6 7
7. co_yield:生成器模式
co_yield 用于实现 生成器模式(Generator Pattern)。它允许函数在执行过程中“产生”一个值并暂停,之后可以从中断处继续运行,非常适合用来实现惰性求值的序列,比如无限数列、数据流、遍历器等。
co_yield expr
相当于:
co_await promise.yield_value(expr)
它先将值传递给 yield_value,然后挂起协程并将值返回给消费者。
典型的 yield_value 实现会将值存储在 Promise 中,并返回 std::suspend_always,以便将控制权交还给调用者。
实现一个通用 Generator
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
T value_;
std::exception_ptr exception_;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
template<std::convertible_to<T> From>
std::suspend_always yield_value(From&& from) {
value_ = std::forward<From>(from);
return {};
}
void return_void() {}
void unhandled_exception() { exception_ = std::current_exception(); }
};
handle_type h_;
explicit operator bool() { /* 检查是否还有下一个值 */ }
T operator()() { /* 获取当前值并推进 */ }
~Generator() { if (h_) h_.destroy(); }
};
使用示例:斐波那契数列
Generator<uint64_t> fibonacci_sequence(unsigned n) {
if (n == 0) co_return;
co_yield 0;
if (n == 1) co_return;
co_yield 1;
uint64_t a = 0, b = 1;
for (unsigned i = 2; i < n; ++i) {
uint64_t s = a + b;
co_yield s;
a = b; b = s;
}
}
int main() {
auto gen = fibonacci_sequence(10);
for (int i = 0; gen; ++i)
std::cout << "fib(" << i << ")=" << gen() << '\n';
}
输出:
fib(0)=0
fib(1)=1
...
fib(9)=34
C++23 已标准化
std::generator,见<generator>
8. 其他
8.1 标准库支持
C++20 引入了 <coroutine> 头文件,提供基础类型支持:
| 类型 | 说明 |
|---|---|
std::coroutine_handle<P> |
协程句柄,用于 resume/destroy |
std::coroutine_traits<R, Args...> |
元编程工具,提取 Promise 类型 |
std::suspend_always |
总是挂起的 awaitable |
std::suspend_never |
从不挂起的 awaitable |
C++23 新增:
| 类型 | 说明 |
|---|---|
std::generator<T> |
同步生成器,替代手写 generator |
8.2 参数与引用的安全问题(Dangling Reference)
由于协程可能延迟执行,若其捕获了局部对象的引用,而该对象已销毁,就会导致未定义行为。
示例:危险的悬空引用
struct S {
int i;
coroutine f() {
std::cout << i; // 使用 i
co_return;
}
};
void bad1() {
coroutine h = S{0}.f(); // S 临时对象立即销毁
h.resume(); // UB:访问已释放的 S::i
h.destroy();
}
正确做法:传值或延长生命周期
void good() {
coroutine h = [](int i) -> coroutine { // i 以值方式传入
std::cout << i;
co_return;
}(0); // i 被复制进协程帧
h.resume(); // 安全
h.destroy();
}
建议:尽量使用值传递参数,避免引用捕获短暂生存期的对象。
9. 线程完整使用示例 – 异步加法任务
我们实现一个简单的 task<int> 类型,表示一个可以异步计算并返回整数结果的任务。
主函数中调用一个协程 add_async(3, 4),它模拟延迟计算 3 + 4,并通过 co_await 获取结果。
该示例展示了:
- 如何定义可等待的协程返回类型
- Promise 的基本实现
co_return和co_await的使用- 异常安全与生命周期管理
#include <iostream>
#include <coroutine>
#include <exception>
// ========================================
// 1. 定义 task<int>:代表一个能返回 int 的异步任务
// ========================================
struct task {
struct promise_type; // 声明嵌套的 promise_type
using handle_type = std::coroutine_handle<promise_type>;
handle_type h_; // 存储协程句柄
explicit task(handle_type h) : h_(h) {}
~task() {
if (h_) {
h_.destroy(); // 销毁协程状态
}
}
task(const task&) = delete;
task& operator=(const task&) = delete;
task(task&& other) noexcept : h_(other.h_) {
other.h_ = nullptr;
}
task& operator=(task&& other) noexcept {
if (this != &other) {
if (h_) h_.destroy();
h_ = other.h_;
other.h_ = nullptr;
}
return *this;
}
// 阻塞等待结果(用于演示,实际可用回调或事件循环)
int get() {
while (!h_.done()) {
h_(); // 恢复协程执行
}
return h_.promise().result_; // 从 promise 中取出结果
}
};
// ========================================
// 2. 实现 promise_type
// ========================================
struct task::promise_type {
int result_; // 存放 co_return 的值
std::exception_ptr ex_ptr_; // 存放抛出的异常
// 创建返回对象(即 task 实例)
task get_return_object() {
return task{handle_type::from_promise(*this)};
}
// 初始不挂起 → 立即开始执行
std::suspend_never initial_suspend() {
return {};
}
// 最终挂起 → 让外部有机会获取结果后再销毁
std::suspend_always final_suspend() noexcept {
return {};
}
// 处理 co_return value
void return_value(int value) {
result_ = value;
}
// 处理未捕获异常
void unhandled_exception() {
ex_ptr_ = std::current_exception();
}
};
// ========================================
// 3. 定义一个简单的协程:异步加法
// ========================================
task add_async(int a, int b) {
std::cout << "正在计算 " << a << " + " << b << "...\n";
// 模拟一些耗时操作(如 I/O 或网络请求)
// 注意:这里只是 sleep,真实场景应结合非阻塞机制
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// 返回结果
co_return a + b;
}
// ========================================
// 4. 主函数:启动并运行协程
// ========================================
int main() {
std::cout << "=== 开始协程测试 ===\n";
// 启动协程
auto t = add_async(3, 4);
// 等待并获取结果
int result = t.get();
std::cout << "计算完成,结果是: " << result << "\n";
std::cout << "=== 程序结束 ===\n";
return 0;
}
可能的输出
=== 开始协程测试 ===
正在计算 3 + 4...
计算完成,结果是: 7
=== 程序结束 ===
关键点说明
| 组件 | 作用 |
|---|---|
task |
用户定义的协程返回类型,封装协程句柄 |
promise_type |
控制协程行为的核心类,决定如何创建返回对象、是否挂起、如何返回值等 |
get_return_object() |
创建并返回 task 对象给调用者 |
initial_suspend() |
返回 suspend_never 表示立即执行协程体 |
final_suspend() |
返回 suspend_always 以便外部读取结果后才销毁 |
return_value() |
接收 co_return 的值并保存到 promise |
co_return |
结束协程并将值传给 return_value |
co_await |
在本例中隐式用于 initial_suspend 和 final_suspend,显式可用于更复杂的 awaitable |
get() 方法 |
驱动协程运行直到完成,并提取结果 |
