在 C++20 里,协程(coroutine)已正式成为标准的一部分。它们为我们提供了在不使用线程或回调的情况下实现异步 I/O 的方式。本文将演示如何使用 std::async 与 std::future、std::filesystem、std::fstream 以及 C++20 协程相关的 co_await、co_return 来实现一个简单的异步文件读取框架,并讨论其优势与局限。
1. 协程基础回顾
协程由以下核心组成:
| 关键字 | 作用 |
|---|---|
co_await |
暂停协程并等待一个 awaitable 对象完成 |
co_yield |
产生一个值并暂停协程,等待下次 co_await |
co_return |
结束协程并返回最终值 |
co_resume |
触发协程继续执行(内部由调度器管理) |
标准库提供了 std::future、std::promise 等类,并配合 std::async 可以直接返回 std::future 对象,用作最简易的异步执行。
2. 异步文件读取的基本需求
- 非阻塞:读取文件时不阻塞主线程,允许继续处理其他任务。
- 流式读取:对于大文件,避免一次性将全部内容读入内存。
- 可组合:能够与其他协程链式调用,形成清晰的异步流程。
3. 设计一个 async_read_file 协程
下面的实现将演示如何:
- 使用
std::filesystem获取文件大小; - 通过
std::ifstream以块方式读取文件; - 通过
co_await暂停直到块读取完成; - 将每块数据返回给调用者。
#include <iostream>
#include <fstream>
#include <filesystem>
#include <vector>
#include <coroutine>
#include <future>
#include <thread>
namespace fs = std::filesystem;
// 1. Awaitable 结构体,用于包装异步读取块
template <typename T>
struct Awaitable {
std::future <T> fut;
Awaitable(std::future <T>&& f) : fut(std::move(f)) {}
bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) noexcept {
// 在后台线程里完成读取
std::thread([f = std::move(fut), h]() mutable {
f.wait();
h.resume();
}).detach();
}
T await_resume() { return fut.get(); }
};
// 2. 读取块的异步函数
std::future<std::vector<char>> async_read_block(std::ifstream& in, std::size_t block_size) {
return std::async(std::launch::async, [&in, block_size]() -> std::vector <char> {
std::vector <char> buf(block_size);
in.read(buf.data(), static_cast<std::streamsize>(block_size));
buf.resize(in.gcount()); // 调整实际读取长度
return buf;
});
}
// 3. 协程包装器
struct AsyncFileReader {
struct promise_type {
AsyncFileReader get_return_object() {
return AsyncFileReader{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
explicit AsyncFileReader(std::coroutine_handle <promise_type> h) : coro(h) {}
~AsyncFileReader() { if (coro) coro.destroy(); }
};
// 4. 主协程函数
AsyncFileReader async_read_file(const fs::path& file_path, std::size_t block_size = 8192) {
std::ifstream file(file_path, std::ios::binary);
if (!file) throw std::runtime_error("Cannot open file.");
while (file) {
// 异步读取块
auto fut = async_read_block(file, block_size);
Awaitable<std::vector<char>> awaiter(std::move(fut));
std::vector <char> chunk = co_await awaiter;
if (chunk.empty()) break;
// 这里可以把块送到下游或直接输出
std::cout.write(chunk.data(), static_cast<std::streamsize>(chunk.size()));
}
}
代码说明
Awaitable:包装std::future,在await_suspend时在后台线程完成等待,随后恢复协程。这样主线程不会被阻塞。async_read_block:使用std::async在后台线程读取指定大小的数据块。AsyncFileReader:定义一个可协程对象的包装器。实际的文件读取逻辑放在async_read_file协程里,利用co_await让读取变得异步。
4. 如何使用
int main() {
try {
async_read_file("bigfile.dat");
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
// 由于协程是异步执行,主线程需要保持活跃或加入同步点
std::this_thread::sleep_for(std::chrono::seconds(1));
}
注意:上例中我们在 main 里使用 sleep_for 保证后台线程有足够时间完成。真实项目中建议使用事件循环或同步机制来终止程序。
5. 与传统 std::async 的比较
| 特点 | 传统 std::async |
C++20 协程 + Awaitable |
|---|---|---|
| 可读性 | 难以串联多步异步 | 代码更像同步,易读 |
| 堆栈占用 | 每个线程占 2-4 MB | 协程共享调用栈,低占用 |
| 错误传播 | 通过 future.get() 抛异常 |
直接抛异常,易捕获 |
| 资源管理 | 需要手动 join / detach | 自动销毁 coroutine handle |
| 性能 | 每次 spawn 线程开销 | 轻量级状态机,几乎无开销 |
6. 局限与未来展望
- 线程池:当前实现使用
std::async直接 spawn 线程。实际应用中建议使用线程池来复用线程。 - 文件系统异步:标准库仅提供同步 I/O。若需要真正的 OS 异步 I/O(如 Linux 的
aio),需结合平台特定 API。 - 错误处理:在协程链中若出现异常,必须确保上层可以捕获并处理。可借助
co_await的异常传播机制实现。 - 调度器:更复杂的异步框架会自定义调度器,决定何时 resume 协程。此处我们使用简易
std::thread。
未来的 C++ 标准会进一步完善协程特性,例如引入 std::generator、std::task 等,使异步编程更易上手。对开发者而言,掌握协程基本概念并结合现有异步 I/O 库(如 Boost.Asio、cppcoro 等)将是实现高性能网络或文件 I/O 的关键。
结语:通过上述示例,我们可以看到 C++20 协程提供了一种既简洁又高效的方式来实现异步文件读取。虽然仍有细节需要完善(如线程池、真正的 OS 异步 I/O),但它已足以在多数业务场景中替代传统回调或线程池模型,显著提升代码可维护性与运行效率。祝你编码愉快!