在现代 C++ 开发中,异步 I/O 已成为高性能网络服务和文件处理的核心技术之一。C++20 引入的协程(coroutines)为实现高效、可读的异步逻辑提供了强大工具。本文将通过一个完整示例,演示如何利用 C++20 协程完成文件的异步读取,并在读取过程中实现分块处理、错误捕获以及资源自动释放。
1. 目标功能
- 异步读取:不阻塞主线程,读取完成后再通知业务层。
- 分块读取:一次读取固定大小块,便于对大文件流式处理。
- 错误处理:捕获文件打开、读取错误并返回异常信息。
- 资源管理:使用 RAII 自动关闭文件句柄。
2. 关键技术点
| 技术 | 说明 |
|---|---|
std::experimental::generator |
用于生成可迭代的协程数据流,C++20 中已标准化为 std::generator. |
std::future/std::promise |
协程与调用方交互的标准方式。 |
std::ifstream |
C++ 标准文件流,配合 std::ios::binary 打开。 |
std::error_code |
统一错误表示。 |
3. 代码实现
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <future>
#include <exception>
#include <filesystem>
#include <coroutine>
// 简易协程生成器(C++20 std::generator 已包含)
template <typename T>
struct generator {
struct promise_type {
T current_value;
std::exception_ptr eptr;
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 = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { eptr = std::current_exception(); }
};
std::coroutine_handle <promise_type> coro;
generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
struct iterator {
std::coroutine_handle <promise_type> coro;
bool done = false;
iterator(std::coroutine_handle <promise_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() {
coro.resume();
return iterator{coro, coro.done()};
}
iterator end() { return iterator{coro, true}; }
};
// 异步读取块
generator<std::vector<char>> async_read_blocks(const std::string& path,
std::size_t block_size = 4096) {
std::ifstream file(path, std::ios::binary);
if (!file) {
throw std::runtime_error("无法打开文件: " + path);
}
while (file) {
std::vector <char> buffer(block_size);
file.read(buffer.data(), static_cast<std::streamsize>(block_size));
std::streamsize bytes = file.gcount();
if (bytes > 0) {
buffer.resize(static_cast<std::size_t>(bytes));
co_yield buffer;
}
}
co_return;
}
// 主程序:使用 std::future 触发协程
std::future<std::size_t> read_file_async(const std::string& path) {
return std::async(std::launch::async, [path] {
std::size_t total_bytes = 0;
try {
for (auto&& block : async_read_blocks(path)) {
// 模拟处理:这里简单累加字节数
total_bytes += block.size();
// 你可以在此处加入更复杂的业务逻辑
}
} catch (const std::exception& e) {
std::cerr << "读取错误: " << e.what() << '\n';
throw; // 重新抛出,future 会携带异常
}
return total_bytes;
});
}
// 示例:读取指定文件并打印总字节数
int main() {
std::string filename = "sample.txt";
try {
auto fut = read_file_async(filename);
std::size_t size = fut.get(); // 阻塞直到完成
std::cout << "文件 '" << filename << "' 共计 " << size << " 字节\n";
} catch (const std::exception& e) {
std::cerr << "异常: " << e.what() << '\n';
}
return 0;
}
4. 代码说明
-
generator- 自定义的协程生成器,用于
co_yield数据块。 - 通过
iterator与标准循环语法配合,实现for(auto&& block : async_read_blocks(...))。
- 自定义的协程生成器,用于
-
async_read_blocks- 打开文件并逐块读取。
- 读取到的数据直接通过
co_yield暴露给调用方。 - 在文件读完后自动销毁协程,释放文件句柄。
-
read_file_async- 使用
std::async将协程包装为std::future。 - 这样主线程不会被阻塞,业务层可以通过
future.get()或future.wait()等方式等待结果。 - 异常在协程内部捕获后重新抛出,
future会携带异常信息。
- 使用
-
main- 调用
read_file_async并等待结果。 - 打印读取的总字节数;若出现错误则输出错误信息。
- 调用
5. 优点与扩展
- 可读性:协程使异步流程像同步代码一样直观。
- 高效:只在需要时才产生新的块,避免一次性读入整个文件。
- 可扩展:可将
co_yield替换为网络 I/O、数据库查询等异步源。 - 错误处理:统一捕获并返回,易于调试。
若想进一步优化:
- 使用
std::filesystem检查文件大小并预估块数。 - 将块大小设为可配置,根据实际磁盘 I/O 性能动态调整。
- 结合
std::async与std::future的链式调用,实现多阶段异步管道。
总结
C++20 协程为异步文件读取提供了极简、可维护的实现方式。通过本示例,你可以快速在项目中集成异步 I/O,并在此基础上构建更复杂的异步处理流水线。