在 C++20 中引入了协程(coroutines)这一强大的语言特性,它让异步编程和延迟计算变得异常简洁。下面从实现细节、编译器支持、以及典型使用场景三方面拆解协程的工作原理。
1. 协程的核心概念
- 协程函数:使用
co_await,co_yield,co_return的函数,返回的类型必须是 协程返回类型(如std::future,std::generator等)。 - 悬挂:在协程体内部遇到
co_await或co_yield时,协程会暂停执行,并把当前状态保存到协程框架中。 - 恢复:外部通过
await_suspend或operator++(对generator)等触发,协程从暂停点继续执行。
2. 编译器如何实现
2.1 生成隐藏的状态机
编译器把协程函数重写为一个 状态机。在编译阶段会:
- 为协程生成一个内部结构体,包含:
- 需要保存的局部变量(如循环计数器、临时对象)。
- 当前状态标记(枚举或整数)。
- 把原函数体拆分成若干块,每块对应一个状态,块之间通过
switch语句跳转。
2.2 协程包装器
协程返回类型(如 std::future、std::generator)内部持有:
- 状态机实例。
- 悬挂器(
promise_type):实现await_ready,await_suspend,await_resume三个接口,用于控制协程的挂起和恢复。
2.3 运行时调度
- 协程挂起:当
co_await或co_yield被执行时,协程会调用promise_type::await_suspend。如果返回true,协程挂起;否则继续执行。 - 调度器:协程的
await_suspend可以接收一个自定义调度器(如std::execution::async),决定何时恢复协程。若未提供,默认同步继续。
3. 与传统异步的区别
- 无回调链:协程隐藏了回调的复杂性,代码像同步一样写。
- 状态持久化:协程的所有局部变量在挂起后会被保存在堆上,避免了手动包装成
std::promise。 - 更轻量:与传统线程或事件循环相比,协程的栈开销极小。
4. 常见协程返回类型
| 类型 | 说明 | 用途 |
|---|---|---|
| `std::future | ||
| ` | 异步操作的结果 | 需要线程池或异步 IO 时 |
| `std::generator | ||
| 生成器,支持co_yield` |
流式数据、迭代器 | |
| `std::task | ||
| `(自定义) | 轻量级异步任务 | 需要自定义调度器时 |
5. 示例:异步文件读取
#include <iostream>
#include <fstream>
#include <filesystem>
#include <experimental/coroutine>
#include <string>
struct async_read_file {
struct promise_type {
std::string buffer;
std::experimental::suspend_always yield_value(const char* data, std::size_t len) {
buffer.append(data, len);
return {};
}
async_read_file get_return_object() {
return async_read_file{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::experimental::suspend_never initial_suspend() { return {}; }
std::experimental::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
std::coroutine_handle <promise_type> h;
async_read_file(std::coroutine_handle <promise_type> h) : h(h) {}
~async_read_file() { if (h) h.destroy(); }
std::string get() { return h.promise().buffer; }
};
async_read_file read_file(const std::filesystem::path& p) {
std::ifstream file(p, std::ios::binary);
const std::size_t chunk = 4096;
char buffer[chunk];
while (file.read(buffer, chunk) || file.gcount() > 0) {
co_yield buffer, file.gcount();
}
}
int main() {
auto reader = read_file("example.txt");
std::cout << reader.get() << std::endl;
}
该示例展示了如何用协程实现分块读取文件,并把结果拼接到字符串中。
co_yield负责把每块数据传回协程外部,编译器自动生成状态机处理挂起与恢复。
6. 小结
C++20 的协程为异步编程提供了极其优雅的语法糖。它通过编译器生成状态机、promise_type 与协程包装器实现协程的挂起与恢复。相比传统回调和线程模型,协程更易读、开销更小。掌握协程后,你可以在网络编程、游戏开发以及任何需要高并发、低延迟的场景中写出更清晰、更高效的代码。