在 C++20 之前,C++ 并没有原生的协程(coroutine)支持,所有异步逻辑都依赖回调、状态机或第三方库(如 Boost.Asio、cppcoro 等)。协程的引入让编写异步代码像写同步代码一样直观。本文以 co_yield 和 co_return 两个关键字为核心,演示如何利用协程实现一个可被异步消费的整数序列生成器,并讨论常见的使用陷阱。
1. 基础概念
- 协程:在执行过程中可以被挂起(yield)或恢复的函数。协程保存其局部状态,允许在不同点继续执行。
co_yield:类似yield,用于向调用者返回一个值并挂起协程。co_return:协程结束时返回最终值,通常是一个void或聚合结果。std::generator(实验性):C++20 标准库提供的协程生成器类型,封装了协程状态和迭代器逻辑。
由于
std::generator仍处于实验阶段,在不同编译器(MSVC、Clang、GCC)中的支持略有差异,本文同时给出自定义实现的版本,便于在任何环境下复现。
2. 示例:异步整数序列生成器
2.1 使用 std::generator(GCC 11+ / Clang 13+)
#include <generator>
#include <iostream>
std::generator <int> async_range(int start, int end, int step = 1) {
for (int i = start; i < end; i += step) {
co_yield i; // 返回当前值并挂起
}
co_return; // 结束协程
}
int main() {
for (int n : async_range(0, 10, 2)) {
std::cout << n << ' '; // 输出: 0 2 4 6 8
}
std::cout << '\n';
}
说明:
async_range通过co_yield逐个生成数值,调用者可以像普通循环一样遍历。- 协程内部的所有局部变量(如
i)在挂起后会被保留,恢复时从上次co_yield的位置继续执行。
2.2 自定义实现(更具可移植性)
#include <coroutine>
#include <exception>
#include <iostream>
template<typename T>
class generator {
public:
struct promise_type {
T current_value;
std::exception_ptr exception;
generator get_return_object() {
return generator{
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(T value) {
current_value = value;
return {};
}
void unhandled_exception() { exception = std::current_exception(); }
void return_void() {}
};
using handle_type = std::coroutine_handle <promise_type>;
generator(handle_type h) : coro(h) {}
generator(const generator&) = delete;
generator(generator&& other) noexcept : coro(other.coro) {
other.coro = nullptr;
}
~generator() { if (coro) coro.destroy(); }
struct iterator {
handle_type coro;
bool done = false;
iterator(handle_type h, bool d) : coro(h), done(d) {
if (coro && !done) {
coro.resume();
if (coro.done()) done = true;
}
}
iterator& operator++() {
if (!done) {
coro.resume();
if (coro.done()) done = true;
}
return *this;
}
T operator*() const { return coro.promise().current_value; }
bool operator==(std::default_sentinel_t) const { return done; }
bool operator!=(std::default_sentinel_t) const { return !done; }
};
iterator begin() {
return iterator{coro, false};
}
std::default_sentinel_t end() { return {}; }
private:
handle_type coro;
};
generator <int> async_range(int start, int end, int step = 1) {
for (int i = start; i < end; i += step) {
co_yield i;
}
co_return;
}
int main() {
for (int n : async_range(1, 5)) {
std::cout << n << ' '; // 输出: 1 2 3 4
}
}
关键点:
promise_type用于维护协程状态;yield_value保存当前值,initial_suspend/final_suspend控制挂起行为。iterator把协程视为可迭代对象,隐藏挂起/恢复细节。- 该实现兼容所有支持 C++20 协程的编译器。
3. 常见陷阱与最佳实践
| 陷阱 | 解释 | 解决方案 |
|---|---|---|
| 协程对象泄漏 | 如果未正确 destroy(),会导致堆栈泄漏。 |
在 RAII 中管理协程句柄,使用类包装(如上例)自动销毁。 |
| 异常传播 | co_yield 后若抛出异常,协程会进入异常状态。 |
在 promise_type 中实现 unhandled_exception,并在迭代器中捕获 exception_ptr。 |
| 多线程使用 | 协程本身不是线程安全的,若多线程访问同一协程,需要同步。 | 每个线程使用独立的协程实例,或者使用互斥锁包装迭代器。 |
| 内存占用 | 每个挂起点保留局部变量,若局部变量巨大,可能导致堆栈膨胀。 | 避免在协程中保存大型对象,改用指针或引用,或使用 std::pmr。 |
| 编译器支持差异 | GCC 的 std::generator 在 11 版后才支持;MSVC 目前仍处于实验。 |
使用自定义实现或使用第三方库(cppcoro、cppcoro-impl)。 |
4. 与传统异步模式比较
| 方式 | 复杂度 | 可读性 | 性能 |
|---|---|---|---|
| 回调链 | 高 | 低 | 低(频繁的堆分配) |
| Promise/Future | 中 | 中 | 中 |
| async/await(协程) | 低 | 高 | 高(无额外分配) |
协程将状态机的拆解、上下文切换等细节封装为编译器层面,消除了手动管理的负担。通过 co_yield,我们可以像写同步代码一样编写异步迭代器,极大提升开发效率。
5. 结语
C++20 协程是一次重要的语言演进,它把异步编程带回了 C++ 的核心。通过 co_yield 与 co_return,我们可以构建简洁、可维护且性能优秀的异步数据流。无论你是想处理文件 I/O、网络请求还是并行计算,协程都能提供一种更自然、更安全的实现方式。希望本文的示例与实战建议能帮助你在项目中快速上手协程,享受更高层次的抽象与更优雅的代码。