在 C++20 中引入的协程(coroutine)为编写高效、可读性更强的异步代码提供了强大工具。本文将从基础概念出发,展示如何利用协程实现异步文件读取与写入,并结合 std::filesystem 与 std::future 构建完整的异步 I/O 流程。通过示例代码,你可以快速把握协程的使用方式,并在实际项目中加以应用。
1. 协程基础回顾
协程是一种轻量级线程,能够在执行过程中挂起(co_await)并在需要时恢复。协程由三大概念组成:
- awaiter:负责提供挂起/恢复逻辑的对象。常见的有
std::future、std::experimental::generator等。 - promise:协程函数返回值的中介,负责在协程结束时提供结果。
- handle:协程的句柄,用来控制协程的生命周期(如
co_await、resume、destroy)。
在异步 I/O 场景中,最常用的 awaiter 是 std::future 或 std::experimental::task(第三方实现)。这里我们使用标准库提供的 std::future 与 std::promise。
2. 异步文件读取
2.1 设计思路
- 读文件:在后台线程中读取文件内容,然后通过
std::promise传递给主线程。 - 协程:调用
co_await等待std::future完成,并返回读取结果。
2.2 代码实现
#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <filesystem>
#include <coroutine>
namespace fs = std::filesystem;
// awaitable 类型,包装 std::future
template<typename T>
struct AwaitableFuture {
std::future <T> fut;
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([h]() { h.resume(); }).detach(); }
T await_resume() { return fut.get(); }
};
// 异步读取文件
AwaitableFuture<std::string> async_read(const fs::path& path) {
std::promise<std::string> prom;
std::future<std::string> fut = prom.get_future();
std::thread([p=std::move(prom), path](){ // 捕获 promise 并在线程中读取
std::ifstream file(path, std::ios::binary);
if (!file) {
p.set_value(""); // 读取失败返回空串
return;
}
std::string data((std::istreambuf_iterator <char>(file)), std::istreambuf_iterator<char>());
p.set_value(std::move(data));
}).detach();
return AwaitableFuture<std::string>{std::move(fut)};
}
// 协程入口
struct AwaitableString {
std::string value;
AwaitableString(std::string&& v) : value(std::move(v)) {}
std::string operator co_await() && { return std::move(value); }
};
AwaitableString read_file_co(const fs::path& path) {
std::string data = co_await async_read(path);
co_return std::move(data);
}
int main() {
auto path = fs::current_path() / "sample.txt";
// 写入一个示例文件
std::ofstream(path) << "Hello, 协程世界!";
// 通过协程读取文件
std::string content = co_await read_file_co(path);
std::cout << "文件内容: " << content << std::endl;
return 0;
}
2.3 说明
async_read在后台线程中完成文件读取,然后通过std::promise把结果写入std::future。AwaitableFuture是一个自定义 awaitable,满足协程的挂起/恢复要求。它在await_suspend中创建一个线程来恢复协程,确保主线程不会被阻塞。read_file_co是协程函数,内部co_await async_read(path)会挂起直到文件读取完成。
3. 异步文件写入
与读取类似,写入也可以使用协程包装。
AwaitableFuture <void> async_write(const fs::path& path, std::string data) {
std::promise <void> prom;
std::future <void> fut = prom.get_future();
std::thread([p=std::move(prom), path, data=std::move(data)]() mutable {
std::ofstream file(path, std::ios::binary);
if (file) {
file.write(data.c_str(), data.size());
}
p.set_value();
}).detach();
return AwaitableFuture <void>{std::move(fut)};
}
AwaitableString write_file_co(const fs::path& path, std::string data) {
co_await async_write(path, std::move(data));
co_return "写入完成";
}
4. 组合使用示例
int main() {
auto path = fs::current_path() / "async.txt";
std::string content = "C++20协程演示!";
std::string write_res = co_await write_file_co(path, content);
std::cout << write_res << std::endl;
std::string read_res = co_await read_file_co(path);
std::cout << "读取结果: " << read_res << std::endl;
return 0;
}
5. 性能与注意事项
- 线程池:示例使用
std::thread的 detach,每次 I/O 调用都会产生一个线程,效率不高。实际项目中建议使用线程池(如tbb::task_arena、boost::asio或自定义线程池)来复用线程资源。 - 错误处理:示例中错误情况直接返回空串或忽略错误。建议在 promise 里使用
set_exception把异常传递给future,协程中通过try/catch捕获。 - 同步与异步的平衡:协程提供了易读的异步代码,但如果 I/O 主要是文件系统操作,操作系统已经提供了非阻塞 I/O(如
aio),可以与协程结合使用以获得更高效的 I/O。
6. 总结
通过上述代码,我们展示了如何使用 C++20 协程实现异步文件读写。关键点在于:
- 用
std::promise/std::future构建 awaitable。 - 在后台线程或线程池中完成 I/O。
- 在协程函数中使用
co_await等待结果。
掌握了这些技巧后,你可以将异步 I/O 迁移到更高级的网络、数据库或 GUI 事件处理中,为 C++ 应用程序带来更高的并发性与更好的代码可维护性。