C++20 引入了协程(Coroutines)这一强大的语言特性,使得异步编程、生成器以及复杂的状态机逻辑可以用更直观、更简洁的语法来实现。下面我们从理论、实现细节、使用场景以及常见坑四个方面,系统性地解析协程的工作原理,并给出示例代码,帮助你快速上手。
1. 何为协程?
协程是一种“可挂起”的函数。与普通函数的执行流程不同,协程可以在任意位置暂停(co_await、co_yield 或 co_return),随后恢复执行,且恢复时会记住之前的局部状态。这样就能把一个“连续的执行流”拆分成若干“断点”,每一次调用都能让协程从上一次停下的地方继续运行。
C++20 对协程的语法支持包括:
co_await:挂起协程,等待一个 awaitable 对象完成。co_yield:生成一个值并挂起协程,常用于实现生成器。co_return:返回协程结果,结束协程。
2. 协程的底层结构
C++ 协程实际上是编译器把一个普通函数拆分成若干 resume points(恢复点)并生成一个“状态机”对象。下面给出简化的步骤:
-
编译阶段
- 编译器将
co_await、co_yield、co_return所在的位置记录为 yield points。 - 对函数的每个
co_yield生成对应的resume函数。 - 生成一个隐藏的 promise 对象,用来维护协程的状态(比如返回值、异常、挂起点)。
- 编译器将
-
运行时阶段
- 调用协程时,编译器会创建一个 `std::coroutine_handle ` 对象,并把控制权交给协程。
- 当协程遇到
co_await、co_yield或co_return时,状态机会把当前状态存储在 promise 对象中,并把控制权返回给调用者。 - 调用者可以再次
resume协程,恢复到最近一次挂起的位置,继续执行。
重要概念
| 名称 | 作用 |
|---|---|
promise_type |
协程的“上下文”,负责管理返回值、异常和挂起点。 |
coroutine_handle |
用来手动控制协程的句柄,支持 resume()、destroy()、done() 等方法。 |
awaitable |
一个可被 co_await 的对象,需要实现 await_ready()、await_suspend()、await_resume()。 |
3. 示例:实现一个简单的生成器
下面的代码演示如何实现一个产生整数序列的协程生成器。
#include <coroutine>
#include <iostream>
#include <optional>
template<typename T>
struct generator {
struct promise_type {
T current_value;
std::suspend_always yield_value(const 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 return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
explicit generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
struct iterator {
std::coroutine_handle <promise_type> coro;
bool operator!=(const iterator& other) const { return coro != other.coro; }
iterator& operator++() {
coro.resume();
if (!coro.done() && !coro.promise().current_value) {
// skip empty values if needed
}
return *this;
}
const T& operator*() const { return coro.promise().current_value; }
};
iterator begin() { return iterator{coro}; }
iterator end() { return iterator{coro, nullptr}; }
};
generator <int> counter(int start, int end, int step = 1) {
for (int i = start; i <= end; i += step) {
co_yield i;
}
}
int main() {
for (auto n : counter(1, 10, 2)) {
std::cout << n << ' ';
}
// 输出: 1 3 5 7 9
}
核心点
co_yield把值写入promise的current_value,然后暂停。generator::iterator通过resume()继续执行,直到遇到下一个co_yield或co_return。
4. 实际应用场景
-
异步 I/O
co_await与std::future或boost::asio::awaitable等库配合,简化异步操作链。- 示例:使用
asio::co_spawn编写异步网络服务器。
-
生成器
- 想实现像 Python
yield那样的惰性序列时,协程是天然的选择。 - 适用于大数据流、图像处理等场景。
- 想实现像 Python
-
状态机
- 将复杂的状态机拆解为若干协程段,减少嵌套与回调地狱。
- 常见于游戏 AI、交互式 UI 等。
-
多线程任务调度
- 协程与线程池配合,减少线程切换开销,实现高并发任务。
5. 常见坑与调试技巧
| 场景 | 常见错误 | 解决办法 |
|---|---|---|
| 异常传播 | co_await 后抛异常导致协程未正常 destroy |
在 promise_type 中实现 unhandled_exception() 并确保调用方在 destroy() 前检查 done()。 |
| 内存泄漏 | 协程句柄未销毁 | 在生成器对象中实现 ~generator() 或使用 std::unique_ptr 包装句柄。 |
| 无限循环 | co_yield 位置不当导致 resume() 何时停止 |
确认 final_suspend() 返回 std::suspend_always 并在调用方检查 coro.done()。 |
| 性能问题 | 协程创建/销毁频繁导致堆分配 | 对协程使用 suspend_always 或 suspend_never 适当调整挂起点,或在对象池中复用句柄。 |
| 调试难度 | 协程的内部状态难以可视化 | 使用 IDE 的 “Coroutine View” 或在 promise_type 中添加日志;也可以将协程拆分为多个小函数。 |
6. 结语
C++20 的协程为异步编程提供了强大且语义清晰的工具,能够让我们用更少的代码书写高并发、低延迟、低耦合的程序。虽然起步时可能会碰到一些实现细节上的坑,但一旦熟悉了 promise_type、coroutine_handle 的交互方式,你将能在多种场景中大显身手。
如果你想进一步深入,建议阅读:
- 《C++20 协程指南》
- 《C++ Concurrency in Action》第二版(协程章节)
- Boost.Asio 官方文档(协程示例)
Happy coding!