在 C++20 里,协程(Coroutine)成为了一项强大而灵活的特性,极大地简化了异步编程和生成器的实现。下面从设计原理、核心概念、标准库支持以及实际编码实例几个角度,逐步剖析 C++ 协程的细节,帮助你快速上手并在项目中有效利用。
一、协程的设计哲学
协程可以被看作是“轻量级线程”,它们支持暂停(co_await、co_yield)与恢复,线程切换的开销极低。C++ 协程的设计核心是:
- 非阻塞:协程在等待某个异步事件时,能够挂起执行,让调用方继续执行其他任务,避免阻塞线程。
- 无状态切换:协程的状态保存在栈帧之外(通常在堆上分配),实现时采用状态机技术。
- 透明的语法:通过
co_await、co_yield等关键字,协程的写法与普通同步代码几乎无差别。
二、核心概念与实现细节
1. 协程句柄(std::coroutine_handle)
协程句柄是对协程本体的引用,负责控制协程的生命周期。它可以:
resume():恢复协程执行。destroy():释放协程资源。done():检查协程是否已完成。
2. 协程 Promise
Promise 是协程内部的数据容器,包含协程的返回值、异常信息以及 await_transform 等成员。协程函数在编译时会被转换成一个返回 promise_type 的结构体。常见成员:
get_return_object():返回协程句柄或其他封装对象。initial_suspend():协程开始时是否立即挂起。final_suspend():协程结束时的挂起点,通常需要co_await std::suspend_always{}。return_value()/return_void():处理co_return。
3. Awaitable 对象
任何满足以下特性的类型都可以被 co_await:
await_ready():返回true则立即继续执行,否则挂起。await_suspend(coroutine_handle):挂起时调用,传入当前协程句柄。await_resume():挂起后恢复执行时调用,返回值作为co_await的结果。
三、标准库支持
C++20 标准库提供了若干与协程相关的工具:
| 组件 | 说明 |
|---|---|
std::suspend_always |
始终挂起的 Awaitable,常用于 initial_suspend 与 final_suspend。 |
std::suspend_never |
永不挂起,常用于快速启动协程。 |
| `std::generator | |
| ` | 用于生成器模式,内部实现为协程。 |
| `std::task | |
| 异步任务包装器,支持co_await`。 |
|
std::future / std::promise |
与协程可配合使用,实现异步结果获取。 |
四、实战案例:异步文件读取
下面给出一个完整的示例,演示如何使用协程进行异步文件读取,结合 std::generator 逐行返回文件内容。
#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <optional>
#include <filesystem>
namespace fs = std::filesystem;
// 简单的异步读取行协程
struct async_line_reader {
struct promise_type {
std::optional<std::string> current_line;
std::string file_path;
async_line_reader get_return_object() {
return async_line_reader{
std::coroutine_handle <promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {
std::exit(1); // 简化错误处理
}
void return_void() {}
};
std::coroutine_handle <promise_type> coro;
bool done = false;
async_line_reader(std::coroutine_handle <promise_type> h)
: coro(h) {}
~async_line_reader() { if (coro) coro.destroy(); }
// 逐行获取
std::optional<std::string> next() {
if (!coro || coro.done()) { done = true; return std::nullopt; }
coro.resume();
if (coro.done()) { done = true; return std::nullopt; }
return coro.promise().current_line;
}
};
// 协程体:读取文件行
async_line_reader read_file_lines(std::string path) {
std::ifstream ifs(path);
if (!ifs.is_open())
co_return;
std::string line;
while (std::getline(ifs, line)) {
co_yield line; // co_yield 会挂起并返回当前行
}
}
int main() {
const std::string path = "example.txt";
if (!fs::exists(path)) {
std::ofstream ofs(path);
ofs << "Hello\n";
ofs << "C++20\n";
ofs << "Coroutines\n";
}
async_line_reader reader = read_file_lines(path);
while (auto line_opt = reader.next()) {
std::cout << *line_opt << std::endl;
}
return 0;
}
代码解读
async_line_reader包装了协程句柄,并提供next()接口逐行读取。read_file_lines作为协程函数,使用co_yield暂停并返回当前行。- 主函数中通过循环调用
next(),实现异步文件读取的效果。
五、协程的性能与注意事项
- 堆分配:协程体的状态通常在堆上分配,频繁创建协程可能导致内存碎片。可以考虑使用对象池或自定义分配器。
- 异常安全:协程 Promise 的
unhandled_exception应妥善处理异常,防止泄漏。 - 可读性:虽然协程语法简洁,但过度嵌套的
co_await可能导致可读性下降。建议保持层次清晰。
六、总结
C++20 协程为异步编程带来了巨大的便利,从生成器到网络 IO,都可以用更直观的语法实现。掌握 Promise、Awaitable、协程句柄的核心概念,以及标准库提供的工具,你就能在项目中写出既高效又可维护的异步代码。祝你编码愉快,玩转 C++ 协程!