在现代C++(C++20及以后)中,协程(coroutine)提供了一种优雅而高效的方式来处理异步操作。相比传统的回调、事件循环或线程池,协程可以让代码更像同步流程,降低复杂度并提升性能。下面我们通过一个完整示例,展示如何使用标准库中的std::future、std::async和协程特性来实现异步文件读取。
1. 环境与依赖
- 编译器:支持C++20协程的编译器(如GCC 11+、Clang 13+、MSVC 19.29+)。
- 标准库:需包含 ` `, “, “, “, “, “ 等。
提示:协程是一个实验性特性,编译时需开启
-fcoroutines(GCC/Clang)或相应编译器标志。
2. 设计思路
- 任务类型:定义一个
async_io_task,它是一个协程,返回std::future<std::string>。 - 协程悬挂:在文件读取过程中,如果文件内容尚未就绪,协程会挂起并返回控制权给主线程。
- 线程池:利用
std::async启动后台线程完成磁盘 I/O,并在完成后唤醒协程。 - 主程序:发起多个
async_io_task并通过std::future::get()等待结果,示范协程的非阻塞特性。
3. 关键代码
#include <coroutine>
#include <future>
#include <fstream>
#include <iostream>
#include <vector>
#include <string>
// 协程中使用的 promise 类型
struct async_io_promise {
std::future<std::string> get_return_object() {
return std::move(_promise.get_future());
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(std::string&& val) { _promise.set_value(std::move(val)); }
void unhandled_exception() { _promise.set_exception(std::current_exception()); }
std::promise<std::string> _promise;
};
// 协程返回类型
using async_io_task = std::coroutine_handle <async_io_promise>;
// 读取文件的协程
async_io_task read_file_async(const std::string& path) {
std::promise<std::string> promise;
auto future = promise.get_future();
// 在后台线程读取文件
std::async(std::launch::async, [promise = std::move(promise), path]() mutable {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) {
promise.set_exception(std::make_exception_ptr(std::runtime_error("打开文件失败")));
return;
}
std::string data((std::istreambuf_iterator <char>(ifs)), std::istreambuf_iterator<char>());
promise.set_value(std::move(data));
});
// 协程挂起,等待 future 完成
co_await std::suspend_always{};
co_return co_await std::move(future);
}
// 主函数演示
int main() {
std::vector <async_io_task> tasks;
std::vector<std::future<std::string>> futures;
// 假设有三个文件需要读取
std::vector<std::string> files = {"file1.txt", "file2.txt", "file3.txt"};
for (const auto& f : files) {
tasks.emplace_back(read_file_async(f));
futures.emplace_back(tasks.back().promise.get_future());
}
// 主线程可以做其他事情
std::cout << "主线程正在做其他事情...\n";
// 等待所有文件读取完成
for (auto& fut : futures) {
try {
std::string content = fut.get();
std::cout << "读取到内容(" << content.size() << "字节)\n";
} catch (const std::exception& e) {
std::cerr << "读取失败: " << e.what() << '\n';
}
}
return 0;
}
4. 代码说明
- promise 与 future:
async_io_promise用来将协程结果包装成std::future,方便主线程等待。 - suspend_always:让协程立即挂起,等待后台线程完成 I/O。
- async:利用
std::async创建后台线程,读取文件后将结果写入std::promise,从而唤醒协程。 - 错误处理:如果文件打开失败,抛出异常并通过
promise.set_exception传递给协程。
5. 性能与可扩展性
- 线程池:示例使用
std::async,但在生产环境可自行实现线程池,避免频繁创建/销毁线程。 - 缓冲区:根据实际需求,可以对
std::string进行更精细的缓冲区管理,减少拷贝。 - 协程复用:多任务可共享同一事件循环,进一步降低系统开销。
6. 结语
通过 C++20 协程与标准库的协作,我们可以在保持代码可读性的同时,获得异步 I/O 的高效实现。协程提供了一个干净的接口,让异步代码几乎与同步代码无异。随着编译器对协程支持的完善,未来在网络编程、文件系统、GPU 计算等领域,这种模式将得到更广泛的应用。