C++20 标准引入了协程(coroutine)这一强大的语言特性,旨在简化异步编程、协程以及生成器等场景。相比传统的回调或基于线程的并发模型,协程通过编译器生成的状态机实现了轻量级、可组合的执行单元。本文将从概念、实现原理、编程实践以及常见陷阱四个方面,系统剖析 C++20 协程,并给出可直接应用的代码示例。
1. 协程基础概念
- 协程函数:使用
co_await、co_yield或co_return的函数。编译器会把它展开为一个状态机,返回一个可调用对象(promise type)。 - 协程句柄:`std::coroutine_handle
`,用于控制协程的生命周期(挂起、恢复、销毁)。
- 协程状态:包括
suspended、running、completed等。
1.1 co_await 与 co_yield
co_await:等待一个 awaitable 对象。等待期间协程挂起,调用方可以继续执行。co_yield:产生一个值,并挂起协程。被co_await的地方可一次性收集所有产生的值。
1.2 Awaitable 对象
任何类型只要实现 await_ready()、await_suspend()、await_resume() 三个成员函数(或全局函数重载)即可被 co_await。
2. 协程实现原理
编译器将协程函数展开为一个类(promise type)和一个状态机函数。简化过程如下:
- 调用协程函数时,返回一个 `std::coroutine_handle
`,并构造 promise 对象。
co_await、co_yield被翻译成调用对应的 awaitable 方法。co_return把返回值存储到 promise 对象中。- 当协程结束时,destroy 方法被调用。
这一过程保证了协程的“轻量级”,因为只有一次堆分配(如果需要)和一个栈帧即可。
3. 实战案例:异步 I/O 与生成器
3.1 异步读取文件
下面给出一个基于 asio 的异步读取文件示例,利用协程隐藏事件循环的细节。
#include <boost/asio.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <experimental/coroutine>
namespace asio = boost::asio;
using asio::ip::tcp;
using std::string;
// Awaitable 读取文件的异步操作
class AsyncReadFile {
public:
AsyncReadFile(const string& path, std::size_t chunkSize)
: file_(path, std::ios::binary), chunkSize_(chunkSize) {}
struct promise_type {
AsyncReadFile* self;
std::vector <char> buffer;
auto get_return_object() {
return AsyncReadFile(self);
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {
std::terminate();
}
};
using handle_type = std::experimental::coroutine_handle <promise_type>;
AsyncReadFile(AsyncReadFile* self) : self_(self) {}
handle_type coroHandle() { return handle_type::from_promise(*self_); }
std::experimental::generator<std::vector<char>> operator()() {
while (file_) {
std::vector <char> chunk(chunkSize_);
file_.read(chunk.data(), chunkSize_);
std::size_t n = file_.gcount();
if (n > 0) {
chunk.resize(n);
co_yield chunk;
}
}
}
private:
std::ifstream file_;
std::size_t chunkSize_;
// promise_type self_;
};
说明:这里演示了如何把文件读取过程包装成一个生成器,利用
co_yield逐块返回数据。可以进一步改造为真正的 awaitable,使用asio::awaitable。
3.2 简易生成器
下面实现一个通用生成器,生成从 0 开始递增的整数。
#include <experimental/coroutine>
#include <iostream>
template<typename T>
class Generator {
public:
struct promise_type {
T value_;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) {
value_ = v;
return {};
}
Generator get_return_object() {
return Generator{std::experimental::coroutine_handle <promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::experimental::coroutine_handle <promise_type>;
explicit Generator(handle_type h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
bool next() {
if (!handle_.done()) handle_.resume();
return !handle_.done();
}
T current() const { return handle_.promise().value_; }
private:
handle_type handle_;
};
Generator <int> count_to(int max) {
for (int i = 0; i <= max; ++i)
co_yield i;
}
使用方式:
int main() {
auto gen = count_to(10);
while (gen.next())
std::cout << gen.current() << ' ';
// 输出:0 1 2 3 4 5 6 7 8 9 10
}
4. 常见陷阱与最佳实践
-
忘记销毁协程句柄
协程对象持有句柄,如果不手动销毁或返回时保证句柄可析构,可能导致内存泄漏。建议使用std::coroutine_handle的 RAII 包装器,或者返回std::future/asio::awaitable。 -
在协程内部使用阻塞操作
co_await的核心是非阻塞等待;如果在协程里调用了阻塞函数,线程将被挂起,失去协程的优势。一定要使用异步 API。 -
过度使用协程
对于简单的同步代码,引入协程会增加编译时间和调试难度。仅在真正需要异步或生成器特性时使用。 -
异常传播
协程内部抛出的异常会被包装到 promise 的unhandled_exception()。如果你需要在调用方捕获,需在外层再次co_await并捕获异常。 -
跨平台标准库实现差异
标准库对std::coroutine_handle的支持在不同编译器/标准库版本间略有差异。建议在使用前检查编译器的协程支持状态(如-std=c++20 -fcoroutines)。
5. 结语
C++20 协程为 C++ 带来了强大的异步编程模型,既可以让代码像同步那样直观,也能在底层实现高性能的事件驱动。通过深入理解协程的语义、状态机实现与 awaitable 机制,程序员可以构建更清晰、更易维护的异步代码。未来随着标准化进一步完善,协程将成为 C++ 生态中不可或缺的工具。祝你在协程的世界里玩得开心、写出高效、简洁的代码!