在C++20中,协程(coroutines)被引入为标准语言特性,允许我们在函数内部“挂起”和“恢复”,极大地方便了异步编程。本文将介绍如何利用协程实现一个通用的异步迭代器(async iterator),并演示其在实际项目中的应用场景。
1. 协程基础回顾
co_await:挂起协程,等待一个 awaitable 对象完成。co_yield:挂起协程,返回一个值给调用者。co_return:结束协程,返回一个值。std::suspend_always / std::suspend_never:控制协程在何时挂起。
协程的实现细节由编译器完成,开发者只需关注 awaitable、generator 等高级抽象。
2. 定义协程返回类型
为了让协程返回一个可迭代的对象,我们需要实现 generator。其基本结构如下:
#include <coroutine>
#include <exception>
#include <iostream>
#include <string>
#include <vector>
// Forward declaration
template<typename T>
class generator;
// Promise type
template<typename T>
struct generator_promise {
T current_value;
std::exception_ptr current_exception = nullptr;
// Initial suspend: never suspend
std::suspend_always initial_suspend() { return {}; }
// Final suspend: never suspend
std::suspend_always final_suspend() noexcept { return {}; }
// Yield value
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
// Return void
void return_void() {}
// Exception handling
void unhandled_exception() {
current_exception = std::current_exception();
}
// Get the generator
generator <T> get_return_object();
};
3. 生成器类
template<typename T>
class generator {
public:
struct promise_type;
using coro_handle = std::coroutine_handle <promise_type>;
explicit generator(coro_handle h) : handle(h) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
generator(generator&& other) noexcept : handle(other.handle) { other.handle = nullptr; }
generator& operator=(generator&& other) noexcept {
if (this != &other) {
if (handle) handle.destroy();
handle = other.handle;
other.handle = nullptr;
}
return *this;
}
~generator() { if (handle) handle.destroy(); }
// Iterator
struct iterator {
coro_handle coro;
bool done = false;
iterator(coro_handle h) : coro(h) {
if (coro) {
coro.resume();
if (coro.done()) done = true;
}
}
iterator& operator++() {
if (coro) {
coro.resume();
if (coro.done()) done = true;
}
return *this;
}
const T& operator*() const { return coro.promise().current_value; }
bool operator==(const iterator& other) const { return done == other.done; }
bool operator!=(const iterator& other) const { return !(*this == other); }
};
iterator begin() { return iterator(handle); }
iterator end() { return iterator(); }
private:
coro_handle handle;
};
实现 get_return_object:
template<typename T>
generator <T> generator_promise<T>::get_return_object() {
return generator <T>{coro_handle::from_promise(*this)};
}
4. 示例:异步读取文件行
下面演示如何利用协程实现异步读取文件行的迭代器。示例使用 std::ifstream,实际项目可替换为网络读取、数据库查询等异步源。
#include <fstream>
#include <optional>
#include <future>
generator<std::string> async_read_lines(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) co_return;
std::string line;
while (std::getline(file, line)) {
// 模拟异步操作:在这里可以放置真正的 awaitable
co_yield line;
}
}
使用方式:
int main() {
for (const auto& line : async_read_lines("sample.txt")) {
std::cout << line << '\n';
}
}
5. 性能与注意事项
-
协程生成器的开销
- 每个
co_yield产生一次挂起,涉及状态机的保存与恢复。若行数极多,频繁挂起可能导致性能下降。 - 解决方案:批量读取(一次读取多行),在内部一次性
co_yield产生一个std::vector<std::string>。
- 每个
-
异常安全
- 协程内部抛出的异常会被
unhandled_exception捕获,并在外部使用handle.promise().current_exception()进行重新抛出。
- 协程内部抛出的异常会被
-
线程安全
- 协程本身不保证线程安全,若在多线程环境下使用,需自行同步或使用
std::async与co_await结合。
- 协程本身不保证线程安全,若在多线程环境下使用,需自行同步或使用
6. 进阶:与 std::future 结合
如果需要在异步迭代器中使用真正的异步 I/O(如网络请求),可以在协程内部 co_await 一个 std::future:
#include <chrono>
generator <int> async_countdown(int start) {
int value = start;
while (value > 0) {
// 异步等待1秒
auto fut = std::async(std::launch::async, [value]{
std::this_thread::sleep_for(std::chrono::seconds(1));
return value;
});
value = co_await fut; // co_await std::future
co_yield value;
}
}
使用时:
for (int v : async_countdown(5))
std::cout << v << " ";
7. 小结
- 协程为 C++20 引入的强大异步编程机制。
- 通过实现自定义
generator,可以轻松构建异步迭代器。 - 在实际项目中,协程可配合异步 I/O、网络通信、数据库查询等,提升代码可读性与性能。
- 注意协程的状态机开销、异常处理与线程安全问题。
希望本文能帮助你快速上手协程实现异步迭代器,为你的 C++ 项目注入新的活力!