协程是现代C++(尤其是C++20及以后)中非常重要的特性,能够让函数挂起和恢复,从而实现更高效的异步编程、生成器以及状态机等功能。下面我们从基本概念到实现细节,全面剖析C++协程的使用方法。
一、协程基础
-
挂起(suspend)与恢复(resume)
协程可以在执行过程中被挂起(suspend),在某个条件满足后再恢复执行(resume)。挂起时协程的局部状态会被保存,以便恢复时继续执行。 -
生成器(generator)
一种典型的协程应用,即通过co_yield一次产生一个值,调用方使用co_await或循环来获取下一个值。 -
任务(task)
通过co_return返回最终结果,或者在协程内部使用co_return或直接返回一个值。
二、C++20 协程关键字
| 关键字 | 用途 |
|---|---|
co_await |
等待异步操作完成,挂起协程 |
co_yield |
生成值,挂起并返回值给调用方 |
co_return |
结束协程,返回最终值 |
co_return; |
仅结束协程,不返回值 |
三、核心概念:promise 与 handle
C++协程的实现基于两个核心概念:
-
promise
协程中co_await、co_yield、co_return等操作都会与promise交互。promise定义了协程的行为,例如何时挂起、恢复、返回值、异常处理等。 -
handle
` 用来管理协程的生命周期。可以使用 `handle.resume()` 恢复协程,`handle.done()` 判断是否结束。
`std::coroutine_handle
四、实现一个简单生成器
下面给出一个完整例子,实现一个产生整数序列的协程生成器。
#include <coroutine>
#include <iostream>
#include <vector>
// 生成器的 promise 类型
template<typename T>
struct Generator {
struct promise_type {
T current_value;
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() {
return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
}
void unhandled_exception() { std::exit(1); }
void return_void() {}
};
std::coroutine_handle <promise_type> handle;
explicit Generator(std::coroutine_handle <promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
bool move_next() {
if (!handle.done()) {
handle.resume();
}
return !handle.done();
}
T current() const { return handle.promise().current_value; }
};
Generator <int> range(int start, int end) {
for (int i = start; i <= end; ++i) {
co_yield i;
}
}
int main() {
for (auto g = range(1, 5); g.move_next(); ) {
std::cout << g.current() << " ";
}
// 输出: 1 2 3 4 5
}
代码说明
promise_type定义了yield_value让协程挂起并保存当前值。initial_suspend与final_suspend分别决定协程是否立即挂起以及结束时的挂起行为。Generator包装了std::coroutine_handle,提供move_next()与current()两个接口,使用者可以像普通迭代器一样使用。range函数示例演示了如何使用co_yield产生一系列整数。
五、协程与异步 I/O
C++协程最常见的场景是与异步 I/O 结合,例如 boost::asio 或自定义 async_wait。示例:
#include <coroutine>
#include <iostream>
#include <chrono>
#include <thread>
struct Timer {
struct promise_type;
using handle_t = std::coroutine_handle <promise_type>;
struct promise_type {
std::chrono::steady_clock::time_point when;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Timer get_return_object() { return Timer{handle_t::from_promise(*this)}; }
void unhandled_exception() { std::exit(1); }
void return_void() {}
};
handle_t handle;
Timer(handle_t h) : handle(h) {}
~Timer() { if (handle) handle.destroy(); }
void start() {
std::thread([h = handle] {
std::this_thread::sleep_until(h.promise().when);
h.resume(); // 完成后恢复协程
}).detach();
}
};
async Task
async_sleep(std::chrono::milliseconds ms) {
Timer timer;
timer.handle.promise().when = std::chrono::steady_clock::now() + ms;
timer.start();
co_await std::suspend_always{}; // 等待 timer 触发
}
注意:标准库中并没有直接提供 std::suspend_always 等待外部事件的实现,通常需要借助第三方库或平台特定的事件循环。
六、错误处理
协程中出现异常时,promise_type::unhandled_exception() 会被调用。可以在此处捕获异常并做日志或状态转移。例如:
void unhandled_exception() {
std::exception_ptr eptr = std::current_exception();
try { std::rethrow_exception(eptr); }
catch (const std::exception &e) {
std::cerr << "协程异常: " << e.what() << '\n';
}
}
七、性能与注意事项
- 栈分配:协程本质上是生成一个堆分配的状态机,对每个协程实例会产生额外内存开销。
- 生命周期:需要注意
handle的生命周期,避免悬挂引用。 - 异常安全:确保在
promise_type::unhandled_exception()中妥善处理异常,防止资源泄露。
八、总结
C++20 协程提供了强大的语法糖,使得异步编程和生成器等复杂逻辑变得更加简洁。通过理解 promise、handle 与 co_await/co_yield/co_return 的交互,你可以在自己的项目中灵活使用协程,提升代码可读性和性能。祝你玩得开心,编码愉快!