协程(coroutine)是C++20标准中引入的强大语言特性,旨在简化异步编程、状态机实现以及流式数据处理。相比传统的回调和线程模型,协程可以在单线程中实现轻量级的并发,减少上下文切换开销,提高代码可读性。本文将从协程的基础概念、典型使用场景、实现原理以及实践示例四个方面进行系统阐述,帮助读者快速掌握C++20协程的实战技巧。
1. 协程基础概念
| 名称 | 说明 |
|---|---|
co_await |
暂停协程并等待一个 awaitable 对象完成 |
co_yield |
暂停协程并返回一个值给调用者,协程可以继续执行 |
co_return |
结束协程并返回结果 |
awaiter |
任何可以被 co_await 的对象,必须实现 await_ready, await_suspend, await_resume |
promise |
协程与外部交互的桥梁,负责管理协程的生命周期与结果 |
协程函数返回一个特殊的类型 std::future 或自定义的 generator、task 等。
2. 典型应用场景
2.1 异步 I/O
在高并发网络服务器中,协程可以以同步方式书写异步 I/O 代码。例如使用 asio::awaitable 或自定义 awaiter 对 std::socket 进行 co_await,避免回调地狱。
asio::awaitable <void> session(tcp::socket sock) {
std::string data;
while (true) {
std::size_t n = co_await sock.async_read_some(asio::buffer(data),
asio::use_awaitable);
if (n == 0) break;
co_await sock.async_write_some(asio::buffer(data),
asio::use_awaitable);
}
}
2.2 生成器(Generator)
协程的 co_yield 可以实现惰性序列生成,替代传统的迭代器。例如 Fibonacci 序列:
generator<std::uint64_t> fib(uint64_t count) {
std::uint64_t a = 0, b = 1;
for (uint64_t i = 0; i < count; ++i) {
co_yield a;
std::tie(a, b) = std::make_pair(b, a + b);
}
}
2.3 状态机(State Machine)
协程能够天然实现有限状态机。每次 co_await 可以表示一次状态转移,代码结构清晰:
task <void> traffic_light() {
while (true) {
// Green
co_await std::chrono::seconds(5);
// Yellow
co_await std::chrono::seconds(2);
// Red
co_await std::chrono::seconds(5);
}
}
3. 协程实现原理
协程的编译实现相当复杂,但对开发者而言只需要关注接口。下面简述核心流程:
-
Promise 对象:编译器为每个协程生成一个
promise_type,该对象保存协程状态、返回值等。调用者通过co_await获得promise_type::get_return_object(),通常是std::future或自定义包装。 -
Awaiter 机制:
co_await会在编译期调用 awaitable 对象的await_ready()判断是否立即完成。若返回false,则await_suspend()被调用,协程挂起;当外部事件完成后,协程通过await_resume()恢复。 -
协程句柄:`std::coroutine_handle
` 用于操作协程(resume、destroy)。`co_return` 触发 `promise_type::return_value` 并标记协程完成。 -
栈帧重排:编译器将协程中的局部变量拆分成两部分:静态(存放在堆或栈的分配器里)和动态(存放在协程帧)。这使得协程可以被挂起后再恢复时,状态保持一致。
4. 实践示例:异步文件读取
下面演示如何使用 C++20 协程实现异步文件读取,结合 asio 的 awaitable 和自定义 awaiter。
#include <asio.hpp>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
using asio::awaitable;
using asio::use_awaitable;
// 自定义 awaiter:异步读取文件
class async_file_reader {
public:
async_file_reader(const std::filesystem::path& path, std::size_t chunk_size)
: path_(path), chunk_size_(chunk_size) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
std::thread([h, this] {
std::ifstream file(path_, std::ios::binary);
if (!file) { h.resume(); return; }
std::vector <char> buffer(chunk_size_);
while (file.read(buffer.data(), buffer.size()) || file.gcount() > 0) {
buffer.resize(file.gcount());
// 这里简化,直接打印到 stdout
std::cout.write(buffer.data(), buffer.size());
buffer.resize(chunk_size_);
}
h.resume();
}).detach();
}
void await_resume() const noexcept {}
private:
std::filesystem::path path_;
std::size_t chunk_size_;
};
awaitable <void> read_file(const std::string& filename) {
co_await async_file_reader(filename, 8192);
}
int main() {
asio::io_context ctx;
ctx.co_spawn(read_file("large_log.txt"));
ctx.run();
}
该示例展示了:
- 自定义 awaiter:实现了
await_ready,await_suspend,await_resume。 - 异步 I/O:使用
std::thread模拟异步读取,真正项目可结合文件 I/O 的异步 API(如std::experimental::filesystem::async_read)。 - 协程使用:在
main中通过asio::io_context::co_spawn启动协程任务。
5. 关注点与常见陷阱
- 资源泄漏:协程结束后需要手动
destroy,否则堆栈帧会保留。建议使用 RAII 包装std::coroutine_handle. - 异常传播:协程内部抛出的异常会在
promise_type::unhandled_exception处理,外部通过std::future获取std::exception_ptr。 - 性能:协程本身轻量,但 awaiter 的实现如果使用同步阻塞会导致性能下降。尽量使用真正的异步 I/O。
6. 小结
C++20 协程为异步编程、生成器和状态机提供了天然、易读的实现方案。通过掌握 awaitable, awaiter 与 promise_type 的协作机制,开发者可以在不牺牲性能的前提下,编写高并发、易维护的现代 C++ 代码。未来的标准扩展(如 std::ranges::views::generator)将进一步丰富协程生态,值得持续关注。