在 C++20 中协程(coroutines)正式被纳入标准库,带来了异步编程的新范式。本文从协程的基本概念出发,逐步深入实现细节,最终给出一个完整的协程示例,帮助读者快速上手并掌握协程的核心技术点。
1. 什么是协程?
协程是一种轻量级的函数,能够在执行过程中暂停(co_await、co_yield 或 co_return)并在以后恢复执行。它们与传统的线程相比,拥有更低的上下文切换成本,且可以在单线程环境下实现异步 IO、数据流处理等功能。
1.1 协程的三大关键字
| 关键字 | 用途 | 示例 |
|---|---|---|
co_await |
等待一个可等待对象(awaitable),并在其完成后恢复协程 |
auto result = co_await async_operation(); |
co_yield |
产生一个值,挂起协程,直到下一个值被请求 | co_yield i; |
co_return |
结束协程并返回最终结果 | co_return final_result; |
1.2 协程与线程的区别
| 维度 | 协程 | 线程 |
|---|---|---|
| 调度 | 由协程库或运行时决定 | 由操作系统调度 |
| 上下文切换 | 仅保存程序计数器、栈指针等少量状态 | 完整的 CPU 状态(寄存器、栈等) |
| 资源占用 | 栈空间可按需分配 | 需要完整栈空间 |
| 并发方式 | 单线程异步 | 多线程并行 |
2. 协程的实现机制
C++ 协程的实现并非直接在语言层面完成,而是通过编译器把协程函数展开成一个状态机。编译器会生成一个结构体(通常叫做“悬挂结构”)来保存协程的内部状态,包括:
- Promise 类型:定义协程的返回值类型、异常处理等。
- 悬挂句柄:`std::coroutine_handle `,用于手动控制协程的生命周期。
- 协程入口:
resume(),让协程从上一次挂起的位置继续执行。
2.1 Promise 类型
promise_type 定义了协程的“宿主”,其成员函数决定协程如何处理返回值、异常、以及挂起/恢复行为。例如:
struct my_promise {
int value_; // 存储最终返回值
my_promise() = default;
~my_promise() = default;
// 必须提供一个 get_return_object(),返回一个能被调用者使用的对象
std::coroutine_handle <my_promise> get_return_object() noexcept {
return std::coroutine_handle <my_promise>::from_promise(*this);
}
// 用于生成协程入口点的初始 suspend
std::suspend_always initial_suspend() noexcept { return {}; }
// 用于协程结束时的最终 suspend
std::suspend_always final_suspend() noexcept { return {}; }
// 设置最终返回值
void return_value(int v) noexcept { value_ = v; }
// 处理异常
void unhandled_exception() {
std::terminate();
}
};
2.2 协程函数展开
假设有一个协程函数:
std::future <int> async_add(int a, int b) {
co_return a + b;
}
编译器会把它展开成类似以下的代码(简化版):
struct async_add_promise {
// 与 my_promise 同理
};
async_add_promise async_add_impl(int a, int b) {
async_add_promise p;
// 计算结果
p.return_value(a + b);
return p;
}
std::future <int> async_add(int a, int b) {
auto handle = std::coroutine_handle <async_add_promise>::from_promise(async_add_impl(a, b));
handle.resume(); // 立即执行到第一次挂起点
// 这里会得到一个 future 对象,供调用方异步等待
return std::future <int>{ /* ... */ };
}
3. 一个完整的协程示例
下面我们实现一个“异步文件读取”协程。假设我们要从磁盘读取一个文件内容,并在读取完成后返回字符串。我们将使用 co_await 结合 std::experimental::awaitable(在 std::execution/std::ranges 中提供)。
注意:实际代码中需要依赖一个可等待的异步 IO 库,如 Boost.Asio 或 C++标准实验性协程库。此处为演示简化实现。
3.1 Awaitable 类型
#include <coroutine>
#include <string>
#include <iostream>
template<typename T>
class awaitable {
public:
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
awaitable(handle_type h) : coro_(h) {}
awaitable(const awaitable&) = delete;
awaitable& operator=(const awaitable&) = delete;
awaitable(awaitable&& o) noexcept : coro_(o.coro_) { o.coro_ = nullptr; }
~awaitable() { if (coro_) coro_.destroy(); }
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> awaiting) {
// 这里可以把 awaiter 关联到真正的 IO 操作
// 简化起见,直接恢复协程
coro_.resume();
}
T await_resume() { return coro_.promise().value_; }
private:
handle_type coro_;
};
template<typename T>
struct awaitable <T>::promise_type {
T value_;
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) noexcept { value_ = v; }
void unhandled_exception() { std::terminate(); }
awaitable get_return_object() noexcept {
return awaitable{std::coroutine_handle <promise_type>::from_promise(*this)};
}
};
3.2 异步文件读取协程
#include <fstream>
#include <filesystem>
#include <sstream>
awaitable<std::string> async_read_file(const std::string& path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) {
co_return std::string(); // 读取失败返回空字符串
}
std::stringstream buffer;
buffer << ifs.rdbuf();
co_return buffer.str();
}
3.3 主程序调用
int main() {
auto read_task = async_read_file("example.txt");
// 这里我们简单地同步等待协程完成
std::string content = read_task.await_resume(); // 直接获取结果
std::cout << "文件内容长度: " << content.size() << std::endl;
return 0;
}
在实际项目中,你会使用事件循环或线程池来异步等待协程完成,而不是直接调用
await_resume()。以上代码仅展示协程的基本使用方式。
4. 常见协程陷阱与调试技巧
| 陷阱 | 解决方案 |
|---|---|
| Promise 对象被销毁 | 确保协程句柄在使用完毕前保持有效,或使用 std::unique_ptr 等智能指针管理 |
| 异常未捕获 | 在 promise_type 的 unhandled_exception 中加入日志或抛出自定义异常 |
| 悬挂句柄泄漏 | 使用 std::coroutine_handle::destroy() 或 std::unique_ptr 自动释放 |
无效的 co_await 对象 |
确认 awaitable 满足 await_ready、await_suspend、await_resume 三个接口 |
调试技巧
- 使用编译器诊断:GCC/Clang 支持
-Wcooperative、-Wcoro等警告,帮助发现协程错误。 - 手动输出协程状态:在
await_suspend和await_resume中打印日志,追踪协程挂起/恢复的时间点。 - 单步调试:IDE(如 CLion、Visual Studio)可以在
co_await处停下,查看悬挂句柄与 Promise 状态。
5. 结语
C++20 的协程为异步编程带来了前所未有的便利。通过学习协程的基本概念、实现机制和实战示例,开发者可以在保持代码可读性与可维护性的同时,构建高性能的异步应用。建议在实际项目中逐步引入协程,先从单一 IO 操作开始,慢慢扩展到更复杂的任务调度、流水线处理等高级场景。祝编码愉快!