在C++20引入协程之后,协程成为了一个极具吸引力的异步编程工具。它不仅让异步代码像同步代码一样直观,而且在性能上往往优于传统的回调或基于线程的实现。本文从协程的底层实现原理出发,结合实际代码示例,帮助读者快速掌握协程的使用方法和常见陷阱。
1. 协程基本概念
协程是一种轻量级的用户级线程,能够在任意位置挂起(co_await、co_yield、co_return)并在以后恢复执行。与传统线程不同,协程的上下文切换只涉及寄存器、栈指针等少量状态,几乎不需要内存拷贝,开销非常小。
2. 协程的三大组件
- Promise
- 用来在协程开始时准备状态,并在协程结束时返回结果。
promise_type是每个协程必须定义的类,编译器会自动使用它来创建和销毁协程句柄。
- 用来在协程开始时准备状态,并在协程结束时返回结果。
- Coroutine Handle
- `std::coroutine_handle `,负责管理协程的生命周期,提供 `resume()`、`destroy()`、`done()` 等操作。
- Suspension Points
- 由
co_await、co_yield、co_return引入,决定协程何时挂起。
- 由
3. 协程的执行流程
- 编译器在遇到
co_await时,将当前函数拆分为若干个“帧”。 - 每个帧对应一段代码,帧之间的状态保存在
promise_type对象中。 co_await语句会调用被 await 的对象的await_ready()、await_suspend()、await_resume()。- 若
await_ready()返回false,则执行await_suspend(),此时协程挂起,调用者可以决定何时恢复。 - 当外部调用
handle.resume()时,协程恢复执行,直至下一个挂起点或结束。
4. 一个完整的异步文件读取示例
#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
namespace fs = std::filesystem;
// 1. awaitable 类型:异步文件读取
struct AsyncFileRead {
std::string path;
std::vector <char> buffer;
std::size_t offset = 0;
bool await_ready() const noexcept { return false; }
// 当协程挂起时将继续的函数包装进一个异步任务
void await_suspend(std::coroutine_handle<> h) const noexcept {
// 简化实现:直接使用同步读取,随后恢复协程
std::ifstream file(path, std::ios::binary);
if (file) {
file.seekg(0, std::ios::end);
std::size_t size = file.tellg();
buffer.resize(size);
file.seekg(0, std::ios::beg);
file.read(buffer.data(), size);
}
h.resume(); // 立即恢复
}
std::vector <char> await_resume() noexcept { return std::move(buffer); }
};
// 2. Promise 结构
struct FileReaderPromise {
std::vector <char> result;
auto get_return_object() {
return std::coroutine_handle <FileReaderPromise>::from_promise(*this);
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(std::vector <char> r) { result = std::move(r); }
};
using FileReaderTask = std::coroutine_handle <FileReaderPromise>;
// 3. 协程函数
FileReaderTask read_file(const std::string& path) {
AsyncFileRead awaitable{path};
co_return co_await awaitable;
}
// 4. 主函数
int main() {
auto handle = read_file("example.txt");
handle.resume(); // 触发文件读取
std::vector <char> data = std::move(handle.promise().result);
std::cout << "读取到 " << data.size() << " 字节\n";
handle.destroy();
}
说明
- 这里的
AsyncFileRead::await_suspend采用同步读取,并立即恢复协程。实际应用中可以将 I/O 操作委托给线程池或平台异步 API。FileReaderTask返回的句柄允许外部控制协程的挂起、恢复和销毁。
5. 常见陷阱与最佳实践
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 对象生命周期 | co_await 的 awaitable 必须在协程生命周期内保持有效 |
让 awaitable 通过值或引用持有在 promise 内部 |
| 悬空协程句柄 | 错误地使用 handle.resume() 后忘记 destroy() |
推荐使用 std::unique_ptr 或 RAII 包装器 |
| 阻塞主线程 | await_suspend 内部同步阻塞会导致协程挂起后仍然阻塞 |
通过异步 I/O 或线程池实现真正的非阻塞 |
| 异常传播 | 协程内的异常不自动捕获 | 在 promise 的 unhandled_exception 里处理或使用 co_return |
6. 与传统异步模型对比
| 特性 | 传统回调 | std::async |
协程 |
|---|---|---|---|
| 可读性 | 低 | 中 | 高 |
| 资源占用 | 高(线程) | 低(线程池) | 极低 |
| 错误处理 | 复杂 | 简单 | 与同步代码同样简洁 |
| 适用场景 | 小型异步任务 | 大量并行计算 | IO 密集型、事件驱动 |
7. 结语
协程为 C++ 程序员提供了一种既直观又高效的异步编程方式。只要掌握好 promise_type、协程句柄以及 awaitable 的三种接口,几乎可以将所有异步任务转化为同步样式的代码。随着标准库不断完善,协程的生态将愈发成熟,值得每位 C++ 开发者投入时间学习与实践。