在 C++20 标准中,协程(coroutine)被正式纳入语言核心,为异步编程和生成器提供了更简洁、更高效的语法。本文将从协程的基本概念、关键字、状态机实现以及实际应用场景四个方面,详细剖析 C++20 协程的使用与实现原理。
1. 协程的基本概念
协程是一种轻量级的用户级线程,它允许函数在执行过程中暂停并在之后恢复。与传统线程相比,协程不需要操作系统调度,切换成本极低。协程主要由三部分组成:
- 协程函数:使用
co_await,co_yield或co_return的函数体。 - 协程句柄(promise):协程的状态管理对象,负责协程生命周期、异常处理以及返回值。
- 协程状态机:编译器根据协程函数生成的状态机实现,维护协程的执行状态。
2. 关键字与基本语法
2.1 co_await
co_await 用于等待一个 awaitable 对象,类似于 async/await 的 await。它会将协程挂起,直到 awaitable 对象完成。
co_await asyncTask();
2.2 co_yield
co_yield 用于生成值,典型用法是实现生成器:
generator <int> getNumbers() {
for (int i = 0; i < 10; ++i)
co_yield i;
}
2.3 co_return
co_return 用于返回协程的最终结果,结束协程执行。
int result = co_return compute();
3. 协程句柄与 Promise 对象
编译器会根据协程函数生成两个类:
- promise_type:用户自定义,存放协程内部状态、返回值等。
- **coroutine_handle **:句柄,指向 promise,提供 `resume()`、`destroy()` 等操作。
3.1 promise_type 必需成员
| 成员 | 说明 |
|---|---|
auto get_return_object() |
返回协程句柄或包装类型 |
std::suspend_always initial_suspend() |
初始挂起行为 |
std::suspend_always final_suspend() |
最终挂起行为 |
void return_value(T value) |
处理 co_return 的值 |
void unhandled_exception() |
异常处理 |
3.2 典型实现:生成器
template<typename T>
class generator {
public:
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 return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle <promise_type>;
generator(handle_type h) : handle(h) {}
~generator() { if (handle) handle.destroy(); }
bool next() { return handle.resume(); }
T value() const { return handle.promise().current_value; }
private:
handle_type handle;
};
4. 协程状态机的实现原理
当编译器遇到协程函数时,实际上它会生成一个状态机:
- 状态变量:用于标记协程的执行点,类似于 switch-case。
- 堆栈/寄存器保存:在挂起时,将必要的局部变量存储在堆上。
- 调用链:
resume()将根据当前状态跳转到对应代码块。
状态机的核心是 co_await 与 co_yield 触发的挂起点。每个挂起点都有一个对应的“resume point”,在 resume() 时,编译器会恢复到上一次挂起的地方继续执行。
4.1 代码示例
generator <int> foo() {
co_yield 1; // 第一次挂起
co_yield 2; // 第二次挂起
co_yield 3; // 第三次挂起
}
编译后大致会变成:
bool foo_body(generator <int>* self) {
switch (self->state) {
case 0: self->state = 1; self->value = 1; return true;
case 1: self->state = 2; self->value = 2; return true;
case 2: self->state = 3; self->value = 3; return true;
default: return false;
}
}
每一次 next() 调用会更新 state 并返回相应的值。
5. 实际应用场景
-
异步 I/O
协程可以与事件循环(如 libuv、asio)配合,实现非阻塞网络通信。async_socket socket; auto data = co_await socket.read_some(buffer); -
生成器
如上所示,用co_yield实现惰性序列,适用于大数据流、文件逐行读取等。 -
协程池
将多个协程包装为任务,使用线程池调度,避免线程上下文切换。 -
游戏脚本
在游戏引擎中,协程可以实现角色行为、动画脚本等。
6. 性能对比
与传统线程相比:
- 创建成本:协程仅需栈帧一次,几乎无额外开销。
- 切换成本:协程切换仅涉及局部变量的保存与恢复,远低于线程上下文切换。
- 内存占用:协程状态机通常几百字节,远小于线程堆栈(几百 KB)。
7. 常见坑与注意事项
- 未定义行为:在
co_yield之后立即访问未初始化的局部变量会导致 UB。 - 异常传播:
promise_type::unhandled_exception必须处理,否则协程崩溃。 - 生命周期管理:句柄销毁需谨慎,避免悬挂。
- 协程对象拷贝:默认不可拷贝,若需要拷贝需自行实现。
8. 结语
C++20 协程为 C++ 提供了一套优雅的异步编程模型。通过深入理解其关键字、句柄与状态机实现,开发者可以在保持代码可读性的同时,写出高效、可维护的异步代码。随着标准库及第三方库的完善,协程将在更广泛的领域得到应用,从网络编程到游戏开发,再到大数据处理,协程都将成为不可或缺的工具。