协程是 C++20 引入的一项强大功能,旨在让异步编程更接近同步代码的写法。它的核心思想是“暂停”和“恢复”,使得函数在执行过程中可以在多个点挂起,随后继续执行,而不需要手动维护状态机。下面从概念、关键特性、标准库支持以及实战示例四个维度来剖析协程。
1. 协程的基本概念
- 暂停点(yield):协程可以在某个点把当前状态保存并返回给调用者。
- 恢复点(resume):调用者在合适时机再次调用协程,协程会从上一次暂停点继续执行。
- 控制流:协程在同一线程内保持同步风格,编译器会把协程拆分为内部状态机。
协程的本质是 “把函数拆分成一系列可中断的步骤”,这与传统的回调、Promise 或 Future 有着显著不同。
2. 关键语法要素
| 关键字 | 作用 |
|---|---|
co_await |
等待一个 awaitable 对象(如 future、generator 等),协程在此点暂停。 |
co_yield |
从协程产生一个值,暂停并返回给调用者。 |
co_return |
结束协程,返回最终结果。 |
co_await + co_return |
与 co_yield 一样,都是暂停点,只是语义不同。 |
2.1 Awaitable
一个类型只要实现了 operator co_await(),并返回一个可以被 await_suspend 和 await_resume 处理的对象,即可成为 awaitable。
2.2 Promise 和 Handle
- Promise:协程的状态容器,保存返回值、异常等。
- Handle:对协程的句柄,使用
co_await、resume、destroy等成员函数来控制协程。
3. 标准库中的协程支持
| 类 | 说明 |
|---|---|
std::suspend_always |
协程在 await_suspend 时始终挂起。 |
std::suspend_never |
协程永不挂起。 |
| `std::generator | |
| 简单的生成器,提供operator*()、operator++()` 等迭代器语义。 |
|
| `std::task | |
| ` | 类似 future,代表一个可能异步完成的值。 |
std::async |
仍然是基于 Future 的异步执行,配合协程使用可以实现更清晰的异步流程。 |
注意:
std::generator和std::task是 C++20 的实验性库,尚未在所有编译器中完整实现,需要使用编译器特定的-std=gnu++20或者开启-fcoroutines。
4. 实战示例:异步文件读取
下面给出一个简单的异步文件读取示例,演示如何使用协程实现非阻塞 I/O。
#include <iostream>
#include <coroutine>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>
struct AsyncRead {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
std::string result;
std::vector <char> buffer;
std::string filePath;
AsyncRead get_return_object() {
return {handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
handle_type coro;
explicit AsyncRead(handle_type h) : coro(h) {}
~AsyncRead() { if (coro) coro.destroy(); }
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> awaiting) {
// 这里模拟异步 I/O:在后台线程读取文件
std::thread([=]() {
std::ifstream in(coro.promise().filePath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
coro.promise().result = ss.str();
awaiting.resume(); // 读取完毕后恢复调用者
}).detach();
}
std::string await_resume() { return coro.promise().result; }
};
AsyncRead read_file_async(const std::string& path) {
co_await std::suspend_always{}; // 允许调用者在此挂起
std::string content = co_await AsyncRead{}; // 异步读取
co_return content;
}
int main() {
auto reader = read_file_async("example.txt");
std::string data = reader.await_resume(); // 阻塞直到文件读取完成
std::cout << "File content:\n" << data << std::endl;
}
实现细节
AsyncRead的await_suspend启动后台线程读取文件,然后在读取完成后恢复调用者。- 主函数通过
await_resume()获得读取结果。- 这个示例演示了协程如何把“异步读取”抽象为类似同步的写法。
5. 协程的优势与注意事项
| 优势 | 说明 |
|---|---|
| 可读性 | 代码像同步代码,易于维护。 |
| 性能 | 协程的状态机通常比回调更高效,避免了堆栈分配。 |
| 可组合 | 多个协程可以通过 co_await 轻松串联。 |
注意
- 异常传播:协程中的异常会被
promise_type::unhandled_exception捕获,需要在 promise 中显式处理。 - 生命周期:协程句柄必须在协程结束前保持有效,通常通过
std::future或自定义 wrapper 管理。 - 编译器支持:不同编译器对协程支持程度不同,务必检查编译器版本与标准选项。
6. 结语
C++20 的协程为异步编程提供了一条新途径,既保持了同步代码的直观,也充分利用了现代 CPU 的并发能力。掌握协程的基本语法与标准库工具后,你可以在网络、I/O、游戏循环等多种场景中实现更简洁、更高效的代码。祝你在协程世界里玩得开心!