协程(Coroutine)在 C++ 23 中成为标准的一部分,它为编写异步、惰性计算以及可组合的控制流提供了更简洁、更高效的手段。本文将从协程的基本概念出发,逐步介绍其语法、关键类型、实现细节,并给出一个完整的实战示例,帮助读者快速掌握协程的使用方法。
1. 协程的基本概念
协程是一种“轻量级线程”,可以在执行期间暂停(yield)并在需要时恢复。与传统的 std::async 或 std::thread 不同,协程的切换是由编译器生成的,开销极低。协程通过 co_await, co_yield 和 co_return 三个关键字实现协作式暂停与恢复。
co_await:等待一个 awaitable 对象完成,若对象未就绪则挂起协程。co_yield:产生一个值并挂起协程,等待下次继续。co_return:返回一个最终值,结束协程。
协程本质上是一个返回类型为 std::coroutine_handle<> 的函数,编译器会在内部生成一个状态机。
2. 关键类型与约束
std::suspend_always/std::suspend_never:可用于控制协程挂起行为。std::promise_type:每个协程生成器都有一个关联的 promise 类型,用来存储结果、异常等。- `std::coroutine_handle `:表示对协程的句柄,可用于启动、检查状态和恢复。
协程函数的签名通常是:
auto generator() -> std::generator <int>; // 需要 C++23
或者手动实现:
struct MyPromise {
int current;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int val) { current = val; return {}; }
void return_void() {}
};
auto generator() -> std::generator <int> { ... } // 依赖标准库实现
3. 协程与传统异步的区别
| 特点 | 传统异步(如 std::future) | 协程 |
|---|---|---|
| 调度 | 由线程池或事件循环手动管理 | 由编译器生成的状态机自动管理 |
| 开销 | 线程切换、锁 | 非抢占式切换,几乎无上下文切换 |
| 代码可读性 | 回调地狱 | 线性可读的同步式写法 |
4. 一个完整的协程示例
下面演示一个使用 std::generator 的协程实现,生成斐波那契数列,并在主线程中异步消费。
#include <iostream>
#include <generator>
#include <chrono>
#include <thread>
// 生成斐波那契数列的协程
std::generator<long long> fib(long long n) {
long long a = 0, b = 1;
for (long long i = 0; i < n; ++i) {
co_yield a; // 产生当前值
std::swap(a, b);
b += a;
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
int main() {
std::cout << "Fibonacci sequence (first 15 numbers):\n";
auto g = fib(15); // 创建协程生成器
for (auto val : g) { // 迭代消费
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}
说明
std::generator<long long>是标准库提供的协程包装,内部已经实现了必要的 promise 类型。co_yield将当前值推送给消费者,同时挂起协程。for (auto val : g)采用范围基循环消费协程,编译器会生成相应的operator++与operator*。
运行结果示例:
Fibonacci sequence (first 15 numbers):
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
5. 高级技巧
5.1 自定义 Awaitable
可以将自定义类包装为 awaitable,实现异步等待。示例:异步文件读取。
struct AsyncRead {
int fd;
std::size_t size;
char* buffer;
std::suspend_always await_ready() const noexcept { return {}; }
std::suspend_always await_suspend(std::coroutine_handle<> h) const noexcept {
// 在 I/O 线程中启动读操作,完成后恢复 h
std::thread([=, h]() {
// 模拟读操作
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// ...实际读写到 buffer
h.resume();
}).detach();
return {};
}
std::size_t await_resume() const noexcept { return size; }
};
5.2 组合协程
协程之间可以相互 co_await,形成嵌套或流水线。
std::generator <int> even_numbers() {
for (int i = 0; i < 10; i += 2) co_yield i;
}
std::generator <int> odd_numbers() {
for (int i = 1; i < 10; i += 2) co_yield i;
}
std::generator <int> all_numbers() {
for (auto e : even_numbers()) co_yield e;
for (auto o : odd_numbers()) co_yield o;
}
6. 性能与注意事项
- 内存占用:协程生成器在栈上分配状态机,避免堆分配,开销低。
- 异常传播:异常会在协程中被捕获并存储在
promise_type::unhandled_exception(),可通过await_resume()重新抛出。 - 生命周期:协程句柄生命周期必须与协程生成器匹配,避免悬空句柄。
7. 小结
C++ 23 中的协程为编写高效、可组合的异步代码提供了强大工具。通过 std::generator 等标准包装,开发者可以用几行代码完成复杂的流式处理。掌握 co_await、co_yield 与 co_return 的使用,理解协程状态机的实现细节,将使你在现代 C++ 开发中更加游刃有余。祝你编码愉快!