在C++20中,协程(coroutines)为异步编程带来了原生语言支持,而co_yield关键字让我们能够轻松实现生成器模式。生成器是一种按需产出值的迭代结构,异步生成器则进一步允许在产出间隙让出线程去处理其他任务。本文将围绕promise类型、协程帧以及co_yield的工作机制,展示如何手写一个可用的异步生成器。

一、协程与co_yield的基本原理
C++20协程并非通过新线程实现并发,而是借助编译器生成的协程帧,将函数执行状态挂起与恢复。当一个函数包含co_yield、co_await或co_return时,它就被识别为协程。co_yield expr本质上会被编译器改写为co_await promise.yield_value(expr),这意味着我们可以通过定制promise类型来控制值的传递方式。
在生成器场景里,promise需要保存最近一次产出的值,并提供一种机制让外部迭代器能够逐步取走这些值。由于协程在co_yield处挂起,调用方不需要一次性跑完整个函数,而是每请求一次才推进到下一个yield点。这种懒求值特性在处理无限序列或大量IO数据时非常有用。
二、定义异步生成器的promise与返回类型
要实现一个生成器,我们需要定义返回类型(如AsyncGenerator),其内部嵌套一个promise_type。promise负责实现get_return_object、initial_suspend、final_suspend、yield_value和return_void等接口。下面的代码展示了一个最简化的整数异步生成器框架。
#include <coroutine>
#include <optional>
struct AsyncGenerator {
struct promise_type {
int current_value;
std::coroutine_handle<promise_type> continuation;
AsyncGenerator get_return_object() {
return AsyncGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit AsyncGenerator(std::coroutine_handle<promise_type> h) : handle(h) {}
~AsyncGenerator() { if (handle) handle.destroy(); }
bool next() {
if (!handle || handle.done()) return false;
handle.resume();
return !handle.done();
}
int value() const { return handle.promise().current_value; }
};
上面代码中,initial_suspend返回suspend_always,表示协程创建后先挂起,等待第一次next调用才启动。yield_value将传入的整数保存到current_value,并再次挂起。外部通过next恢复协程,用value读取产出值。
这种结构虽然简单,但已经具备生成器核心能力。如果希望它“异步”,只需在yield_value或恢复点插入co_await某个可等待对象,比如模拟IO就绪的awaitable,就能在等待期间释放线程。
三、让生成器真正异步:封装可等待对象
纯同步的生成器在yield之间不会让出线程,若要异步,需要协程在产出前co_await一个代表异步事件的awaitable。下面我们定义一个简单的定时让出awaitable,用来模拟非阻塞延迟。
#include <chrono>
#include <thread>
struct AsyncYield {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
std::thread([h]() mutable {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
AsyncYield在await_suspend中启动一个临时线程模拟异步回调,并在10毫秒后恢复协程。虽然真实项目应使用事件循环而非裸线程,但这足以说明co_await如何与co_yield配合。接下来我们改写生成器函数,让它在每次产出前异步等待。
AsyncGenerator count_async(int max) {
for (int i = 0; i < max; ++i) {
co_await AsyncYield{};
co_yield i;
}
}
在count_async中,每次循环先co_await AsyncYield{}让出执行权,再co_yield i将数字交给调用方。调用方的主逻辑不会被长时间占用,因为等待发生在独立线程中。这样便实现了“异步生成器”的基本形态。
四、使用场景与完整调用示例
假设我们需要从网络流式拉取数据并逐条处理,使用异步生成器可以避免把全部响应读入内存。下面示例在主函数中驱动生成器,并输出收到的数值。
#include <iostream>
int main() {
auto gen = count_async(5);
while (gen.next()) {
std::cout << "got: " << gen.value() << std::endl;
}
return 0;
}
运行后程序会依次打印0到4,每次获取之间间隔约10毫秒。由于协程帧保存了局部变量i和挂起位置,循环状态在多次next调用间自然延续,代码可读性远高于手写状态机。
需要注意的是,当前实现未处理异常传播和协程取消。在生产环境中,应在promise的unhandled_exception中捕获并存储异常,由next调用方决定抛出;同时提供cancel方法强制destroy协程帧,防止资源泄漏。
五、优缺点与适用边界
使用C++20协程实现异步生成器的优势在于语法直观,循环和yield写法与同步代码几乎一致,降低了异步流程的理解成本。编译器自动管理协程帧,省去手动维护状态枚举的麻烦。此外,co_await可无缝接入现有异步IO库,如基于epoll或io_uring的awaitable。
缺点是协程帧分配在堆上,频繁创建大量生成器会带来内存与分配器压力;同时目前标准库未提供官方generator,所有promise逻辑需自行实现,容易在initial_suspend或final_suspend语义上出错。对于极致性能且生命周期极短的场景,传统回调可能更合适。总体来看,在流式处理、懒加载配置或测试数据构造中,co_yield异步生成器是简洁且高效的方案。
C++20coroutinesco_yield修改时间:2026-08-07 10:57:32