协程(Coroutines)是 C++20 标准中正式加入的一个强大特性,它为异步编程提供了一种更直观、更高效的方式。相比传统的基于回调或线程池的异步实现,协程能让代码保持同步的写法,同时避免了“回调地狱”和上下文切换的开销。本文将从协程的基本概念、关键关键词、典型实现以及实际应用几个角度,帮助读者快速掌握协程的核心要点。
1. 协程的基本概念
- 挂起(Suspend):协程在执行过程中可以主动挂起,让出控制权。
- 恢复(Resume):挂起后的协程可以被外部或内部恢复继续执行。
- 状态机:协程内部的执行流被编译器转换为状态机,保持挂起点的上下文。
协程与线程不同,它们是轻量级的任务单元,切换的成本几乎为零。协程的“挂起点”由 co_await, co_yield, co_return 三个关键字来标记。
2. 关键关键词解读
| 关键词 | 作用 | 典型用法 |
|---|---|---|
co_await |
挂起协程,等待一个可等待对象(Awaitable)完成 | int value = co_await async_fetch(); |
co_yield |
暂停协程并返回一个值给调用者,类似生成器 | co_yield i; |
co_return |
结束协程,返回最终值 | co_return result; |
可等待对象(Awaitable)必须实现 await_ready(), await_suspend(), await_resume() 三个成员函数。标准库提供了如 std::future, std::generator, std::task 等实现,也可以自定义。
3. 协程的实现细节
3.1 编译器生成的状态机
编译器会把协程函数拆分为若干块,每个块对应一个 co_await, co_yield, co_return 的位置。状态机内部维护一个 promise_type 对象,保存协程的局部变量、异常信息和返回值。协程入口 operator() 会先调用 promise_type::get_return_object() 获取协程句柄,然后直接执行到第一个挂起点。
3.2 协程句柄(std::coroutine_handle)
句柄是协程的运行时入口,提供 resume(), destroy(), done() 等成员函数。通过句柄可以手动控制协程的执行。
std::coroutine_handle <promise_type> h = coro(); // 启动协程
while (!h.done()) h.resume(); // 逐步恢复
h.destroy(); // 释放资源
在异步框架中,句柄通常与事件循环(Event Loop)结合使用,按需恢复协程。
4. 常见协程模型
| 模型 | 说明 | 示例 |
|---|---|---|
| 协程+事件循环 | 事件循环驱动协程的恢复,适合 I/O 密集型 | asio::co_spawn |
| 协程+线程池 | 线程池负责执行耗时操作,协程负责协作 | std::async 与 co_await |
| 协程+生成器 | 通过 co_yield 实现惰性序列 |
`std::generator |
| seq()` |
5. 实际案例:异步文件读取
下面给出一个完整示例,演示如何使用 co_await + asio(Boost.Asio 1.75+ 或 standalone ASIO)实现异步文件读取。
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/detached.hpp>
#include <fstream>
#include <iostream>
using namespace boost::asio;
using awaitable = awaitable<void, io_context::executor_type>;
awaitable read_file(const std::string& path) {
// 打开文件
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
co_return; // 文件不存在
}
// 获取文件大小
std::size_t size = static_cast<std::size_t>(file.tellg());
file.seekg(0, std::ios::beg);
// 读取内容
std::vector <char> buffer(size);
co_await async_read(
/* handler */,
buffer,
use_awaitable);
std::cout << "Read " << buffer.size() << " bytes.\n";
co_return;
}
int main() {
io_context io;
co_spawn(io, read_file("example.txt"), detached);
io.run();
}
注意:示例中使用了
use_awaitable来将传统异步 API 转化为 Awaitable 对象,简化了协程与 I/O 的耦合。
6. 性能对比
| 场景 | 回调 | 线程池 + std::async |
协程 |
|---|---|---|---|
| 线程切换 | 1 次/任务 | 1 次/任务 | 0 次/任务 |
| 代码可读性 | 低 | 中 | 高 |
| 资源占用 | 线程栈 | 线程栈 | 协程栈 ~ 几 KB |
| 错误传播 | 通过回调传递 | 异常跨线程 | 异常直接抛出 |
从表格可以看出,协程在 I/O 密集型任务中能显著降低上下文切换成本,同时保持代码的同步结构。
7. 进阶话题
- 协程与 RAII:协程内部资源的生命周期管理要靠
promise_type或者自定义析构逻辑。 - 协程池:类似线程池,协程池可预分配协程句柄,降低频繁创建的开销。
- 与现有框架的结合:如
cppcoro,folly::coro,Qt的QFuture等。
8. 小结
- 协程是 C++20 引入的轻量级异步机制,使用
co_await,co_yield,co_return控制挂起与恢复。 - 关键在于实现 Awaitable 对象和事件循环。
- 与传统回调相比,协程具有更好的可读性和更低的运行时成本。
掌握协程后,你可以将异步 I/O、网络通信、并发计算等场景写得更简洁、更高效。未来的 C++ 程序员,协程已成为不可或缺的技能之一。