协程是 C++20 引入的一个强大特性,它通过语言层面的支持,让异步编程变得既简洁又直观。下面通过一个具体的“懒加载文件行读取”示例,来展示协程在日常编程中的实用价值。
1. 什么是协程?
协程(Coroutine)是一种可以暂停与恢复执行的函数。它不再像传统函数那样一次性完成全部工作,而是在需要时挂起(yield),随后可以在同一状态下继续执行。C++20 通过 co_await、co_yield 和 co_return 关键字,以及 std::experimental::coroutine(在标准库中被移至 std::coroutine)实现协程。
2. 目标需求
我们想实现一个懒加载的文件行读取器:
- 懒加载:文件只有在真正需要读取下一行时才去读取,避免一次性把整个文件读入内存。
- 可迭代:可以像普通范围一样使用
for循环遍历。
3. 关键技术点
generator模板:一个简单的协程返回类型,包装协程句柄、生成器值。co_yield:用于把每一行返回给调用者,同时挂起协程。co_return:在文件末尾结束协程。
4. 代码实现
#include <iostream>
#include <fstream>
#include <string>
#include <coroutine>
// 1. 简单的 generator 实现
template<typename T>
struct generator {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T current_value;
std::exception_ptr exception;
auto get_return_object() {
return generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() noexcept {}
void unhandled_exception() {
exception = std::current_exception();
}
};
handle_type coro;
explicit generator(handle_type h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
// 禁止拷贝
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
// 允许移动
generator(generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
generator& operator=(generator&& other) noexcept {
if (this != &other) {
if (coro) coro.destroy();
coro = other.coro;
other.coro = nullptr;
}
return *this;
}
// Iterator 结构
struct iterator {
handle_type coro;
bool done;
explicit iterator(handle_type h, bool d) : coro(h), done(d) {}
iterator& operator++() {
coro.resume();
done = coro.done();
return *this;
}
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() {
if (coro) {
coro.resume();
if (coro.done()) return iterator{coro, true};
return iterator{coro, false};
}
return iterator{coro, true};
}
iterator end() { return iterator{coro, true}; }
};
// 2. 协程函数:懒加载文件行
generator<std::string> lazy_read_lines(const std::string& path) {
std::ifstream fin(path);
if (!fin.is_open()) {
throw std::runtime_error("Cannot open file: " + path);
}
std::string line;
while (std::getline(fin, line)) {
co_yield line; // 暂停协程,返回当前行
}
// 文件读取完毕,协程结束
}
// 3. 使用示例
int main() {
try {
auto lines = lazy_read_lines("example.txt");
for (const auto& line : lines) {
std::cout << line << '\n';
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
return 0;
}
5. 代码解读
- generator:封装了协程句柄和 iterator,提供
begin()/end()使其能与范围for配合。 - lazy_read_lines:打开文件后循环读取每行,使用
co_yield将行返回给调用者。因为co_yield之后协程挂起,文件读取操作保持懒惰。 - main:直接遍历
generator对象即可像处理普通容器一样读取文件行。
6. 性能与优势
- 内存占用:只保持当前行,适用于大文件。
- 代码简洁:省去了手动维护状态机或迭代器类。
- 可组合性:协程可以与
std::async、网络 I/O 等异步操作无缝结合。
7. 进一步拓展
- 错误处理:在协程内部捕获异常并通过
promise_type::unhandled_exception传播。 - 多线程:在协程内部使用
co_await std::suspend_until等实现并发读取。 - 标准库支持:C++23 已将
std::generator定义在标准库中,代码可进一步简化。
8. 小结
C++20 的协程为我们提供了一种既强大又优雅的异步编程方式。通过上述懒加载文件行读取的例子,展示了协程在实际项目中的实用价值。掌握好协程的语法与实现细节,可以让你的 C++ 代码在性能和可维护性上都有显著提升。