C++20 引入了协程(Coroutine)这一强大的语法扩展,它让异步编程与生成器的实现变得异常简洁。协程本质上是一种可以挂起和恢复的函数,它在执行过程中能够“暂停”,并在需要时继续执行。相比传统的基于回调或基于线程的异步方案,协程可以让代码保持同步写法,同时降低资源消耗。下面从实现细节、关键语法以及典型使用场景三个角度来介绍 C++ 协程。
1. 协程的基本实现原理
协程的实现依赖于编译器在函数内部自动生成状态机。简单来说,编译器会把协程函数拆分成若干个基本块,并在每个 co_await、co_yield 或 co_return 处插入状态标记。当协程挂起时,当前的调用上下文(包括局部变量、栈帧等)会被打包到一个协程对象中,随后返回控制权给调用者。再次调度时,协程对象会恢复保存的上下文,从上一次挂起点继续执行。
在 C++20 标准库中,协程的核心抽象是 std::coroutine_handle。它类似于普通的函数指针,但可以指向正在挂起的协程。协程的返回类型通常是 std::future、std::generator 或用户自定义的 Promise 类型。
2. 关键语法要点
| 关键字 | 说明 |
|---|---|
co_await |
挂起协程,等待一个 Awaitable 对象完成。Awaitable 对象需实现 await_ready()、await_suspend() 与 await_resume()。 |
co_yield |
生成一个值并挂起协程,适用于生成器。 |
co_return |
结束协程,返回一个值给 Promise。 |
co_value(C++23) |
从 Awaitable 直接获得一个值,类似于 co_return 与 co_await 的组合。 |
2.1 Awaitable 对象示例
struct TimerAwaitable {
std::chrono::milliseconds delay;
bool await_ready() const noexcept { return delay.count() == 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, delay=delay]{
std::this_thread::sleep_for(delay);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
使用示例:
std::future <void> async_sleep(std::chrono::milliseconds ms) {
co_await TimerAwaitable{ms};
}
2.2 生成器实现
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 = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
Generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
bool next() {
if (!coro.done()) coro.resume();
return !coro.done();
}
T value() const { return coro.promise().current_value; }
};
调用:
Generator <int> count_up_to(int n) {
for (int i = 0; i <= n; ++i)
co_yield i;
}
3. 常见使用场景
-
异步 I/O
与传统asio::awaitable类似,协程可以直接co_await网络读取/写入操作,避免了回调地狱。比如使用 Boost.Asio 的协程适配器:auto socket = co_await boost::asio::async_connect(socket, endpoint); std::size_t n = co_await boost::asio::async_read(socket, buffer); -
数据流处理
通过co_yield可以实现惰性流式数据处理,例如解析大文件、计算统计指标。与std::ranges结合使用,可得到更强大的组合能力。 -
状态机实现
协程的状态机生成天然适合实现复杂协议或游戏 AI。通过co_yield或co_await对外暴露状态切换,代码可读性大幅提升。 -
并发编程
协程可以与std::async、std::thread混合使用,或者作为轻量级任务调度器的核心。利用co_await等待其他协程完成,实现“协作式多任务”。 -
延迟计算与缓存
延迟生成(lazy evaluation)是协程的一大优势。结合std::generator可以在需要时动态生成数据,避免一次性加载导致的内存峰值。
4. 性能与注意事项
- 栈分配:协程对象本身是在堆上分配的状态机。每个协程的栈帧由编译器根据代码生成,通常比线程小得多,支持成百上千协程同时存在。
- 同步与异步:若使用
co_await对一个立即完成的对象,协程会立即继续,几乎不产生开销。对于真正的异步 I/O,需要合适的事件循环。 - 异常处理:协程内抛出的异常会被传递给 Promise 的
unhandled_exception,可在 Promise 中做自定义处理。未捕获的异常会终止程序。 - 调试支持:由于协程是编译器生成的内部状态机,调试器往往无法直接展示调用栈。建议在关键处插入日志或使用专门支持协程调试的工具。
5. 未来展望
C++23 将引入 co_value、std::ranges::coroutine 等新特性,进一步简化协程的使用。与此同时,第三方库如 cppcoro、folly::coro 等提供了更完整的协程生态,满足高性能网络、数据库等领域需求。
综上所述,C++ 协程在提高代码可读性、降低资源消耗、实现高并发等方面具有显著优势。掌握协程的语法与实现细节,将使 C++ 开发者在现代异步编程中更加得心应手。