C++20 协程
理解协程作为可挂起函数的概念。学习 co_yield、co_return、co_await、生成器模式以及异步 I/O 和状态机等实际用例。
什么是协程?
协程是一种可以挂起执行并在之后恢复的函数。与从头运行到尾的普通函数不同,协程可以在执行中途暂停、产出中间结果,并从中断的地方继续。这与线程有根本区别:协程是协作式的(它们选择何时挂起)、轻量的(无需操作系统线程)且确定性的(没有并发访问导致的数据竞争)。
C++20 通过三个新关键字在语言层面引入了协程支持:co_yield(挂起并产出一个值)、co_return(以最终值完成协程),以及 co_await(挂起直到异步操作完成)。任何包含这些关键字之一的函数都自动成为协程。
生成器模式
生成器是一种惰性产出值序列的协程。每次调用 co_yield 都会挂起协程并向调用者传递一个值。以下是一个完整的、可编译的生成器实现:
#include <iostream>
#include <coroutine>
#include <optional>
// A simple Generator type
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{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 value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// Move only
Generator(Generator&& other) noexcept : handle(other.handle) { other.handle = nullptr; }
Generator& operator=(Generator&&) = delete;
Generator(const Generator&) = delete;
bool next() {
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().current_value; }
};
// A coroutine that generates Fibonacci numbers
Generator<long long> fibonacci() {
long long a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// A coroutine that generates a range
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
int main() {
auto fib = fibonacci();
std::cout << "First 10 Fibonacci numbers: ";
for (int i = 0; i < 10 && fib.next(); ++i) {
std::cout << fib.value() << " ";
}
std::cout << "\n"; // 0 1 1 2 3 5 8 13 21 34
auto r = range(1, 6);
while (r.next()) {
std::cout << r.value() << " "; // 1 2 3 4 5
}
std::cout << "\n";
return 0;
}协程机制:Promise 与 Handle
每个协程都有两个关键组件:
promise_type ── 控制协程的行为。编译器在协程的返回类型中查找嵌套的 promise_type。关键方法:
- get_return_object() ── 创建返回给调用者的对象
- initial_suspend() ── 返回 suspend_always(惰性启动)或 suspend_never(急切启动)
- final_suspend() ── 协程结束时执行的操作(必须是 noexcept)
- yield_value(T) ── 在 co_yield 时被调用
- return_value(T) 或 return_void() ── 在 co_return 时被调用
- unhandled_exception() ── 在异常逃逸时被调用
std::coroutine_handle ── 指向协程帧的类型擦除指针。提供 resume()、done() 和 destroy() 方法。协程帧存活在堆上,包含所有局部变量和挂起点。
流程为:调用者创建协程 -> initial_suspend -> 调用者调用 resume() -> 协程运行直到 co_yield/co_return/co_await -> 控制返回调用者 -> 重复。
co_await 与可等待对象
co_await 挂起协程直到异步操作完成。任何实现了 Awaitable 接口的类型都可以与 co_await 一起使用。这是异步 I/O 框架的基础:
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
// A simple awaitable that simulates async work
struct AsyncTimer {
std::chrono::milliseconds duration;
bool await_ready() const noexcept {
return duration.count() <= 0; // skip suspend if no wait needed
}
void await_suspend(std::coroutine_handle<> handle) const {
// In production, this would register with an event loop
// Here we simulate with a thread
std::thread([handle, d = duration]() {
std::this_thread::sleep_for(d);
handle.resume(); // resume the coroutine
}).detach();
}
void await_resume() const noexcept {
// Called when the coroutine resumes — return value becomes
// the result of the co_await expression
}
};
// The three Awaitable methods:
// 1. await_ready() — can we skip suspending? (optimization)
// 2. await_suspend() — what to do when suspending
// 3. await_resume() — what value to produce when resuming
// A Task type for async coroutines
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_work() {
std::cout << "Starting async work...\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 1 complete\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 2 complete\n";
}
int main() {
async_work();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return 0;
}实际用例
协程在多个实际场景中表现出色:
生成器 / 惰性序列 ── 按需产出值,无需预先计算整个序列。非常适合无限序列、文件解析和数据管道。
异步 I/O ── 网络服务器、数据库查询和文件操作可以 co_await 完成而不阻塞线程。cppcoro、libunifex 和 Boost.Asio 等库提供了支持协程的 I/O 原语。
状态机 ── 每个 co_yield 或 co_await 代表一个状态转换。协程的局部变量自然地在转换之间保持状态,消除了手动状态结构体。
事件驱动编程 ── UI 框架和游戏循环可以使用协程来表达跨越多个帧或事件的顺序逻辑。
解析 ── 词法分析器和解析器可以在处理输入时 co_yield 标记或 AST 节点,隐式地维护解析状态。
协程和线程解决不同的问题:
使用协程的场景:需要协作式多任务、惰性求值或无操作系统线程开销的异步 I/O。协程没有上下文切换开销,不需要同步原语,并且可以扩展到数百万个并发任务。
使用线程的场景:需要跨 CPU 核心的真正并行来处理 CPU 密集型工作,或者需要抢占式调度(一个永不挂起的协程会阻塞其线程)。
最佳实践:对 I/O 密集型并发使用协程,对 CPU 密集型并行使用线程。许多高性能服务器将两者结合:线程池运行协程,每个协程使用 co_await 处理一个连接的 I/O。
悬空引用:协程参数被拷贝到帧中,但对临时对象的引用可能悬空。始终按值传递参数或确保被引用对象的生命周期超过协程。
堆分配:每个协程都在堆上分配一个帧。对于非常热的、短生命周期的协程,这可能是性能问题(尽管编译器可以通过堆分配省略优化 ── HALO 来优化)。
调试困难:协程栈在调试器中更难检查。挂起状态在堆上而非调用栈上。使用日志和结构化并发来保持可调试性。
无标准库类型:C++20 提供了语言原语但没有标准的 Generator 或 Task 类型。你必须自己编写或使用库。C++23 添加了 std::generator。
- 协程是可挂起函数 ── 它们通过
co_yield/co_await暂停并在之后恢复 - 生成器模式产出惰性序列:每个
co_yield挂起并传递一个值 co_await挂起直到异步操作完成 ── 可等待对象的三个方法控制行为promise_type和coroutine_handle构成连接协程与调用者的机制- 协程是协作式的(无抢占)且轻量的(无操作系统线程)── 非常适合 I/O 密集型并发
- C++20 仅提供原语;使用
std::generator(C++23)或 cppcoro 等库进行生产代码开发
测验 — 检验你的知识
(20 经验值)1. 哪个关键字使函数在 C++20 中成为协程?
2. `promise_type::yield_value(T)` 的目的是什么?
3. 协程与线程有何不同?