在C++20中,协程(coroutines)被正式纳入标准库,提供了更直观、更高效的异步编程方式。下面我们通过一个完整的示例,演示如何使用协程实现异步文件读取,并利用std::filesystem和std::async来完成高性能的文件处理。
1. 关键概念回顾
- 协程函数:返回
std::future、std::generator或自定义类型,内部使用co_await、co_yield和co_return实现挂起与恢复。 std::filesystem:用于跨平台文件系统操作,读取文件大小、遍历目录等。std::async:在后台线程中执行耗时任务,结合协程可实现真正的异步。
2. 设计思路
- 定义一个异步读取器:
async_read_file(const std::string& path)返回std::future<std::string>,在后台线程读取文件内容。 - 主协程:使用
co_await等待文件读取完成,然后对内容进行进一步处理(如字符串统计、哈希计算等)。 - 错误处理:使用
std::exception_ptr捕获文件打开错误或读取异常,确保协程异常安全。
3. 代码实现
#include <iostream>
#include <string>
#include <future>
#include <filesystem>
#include <fstream>
#include <exception>
#include <coroutine>
#include <vector>
#include <algorithm>
namespace fs = std::filesystem;
// 简单的协程包装器,用于异步执行任务
template<typename T>
struct AsyncTask {
struct promise_type {
std::promise <T> promise;
AsyncTask get_return_object() { return {promise.get_future()}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(T value) { promise.set_value(std::move(value)); }
void unhandled_exception() { promise.set_exception(std::current_exception()); }
};
std::future <T> fut;
AsyncTask(std::future <T>&& f) : fut(std::move(f)) {}
operator std::future <T>&&() { return std::move(fut); }
};
// 异步读取文件内容
AsyncTask<std::string> async_read_file(const std::string& path) {
co_await std::suspend_always{}; // 确保进入后台线程
try {
if (!fs::exists(path)) {
throw std::runtime_error("文件不存在: " + path);
}
std::ifstream ifs(path, std::ios::binary | std::ios::ate);
std::streamsize size = ifs.tellg();
ifs.seekg(0, std::ios::beg);
std::string buffer(size, '\0');
if (!ifs.read(&buffer[0], size)) {
throw std::runtime_error("读取文件失败: " + path);
}
co_return buffer;
} catch (...) {
co_return std::string(); // 空字符串表示错误
}
}
// 主协程:读取文件并统计字符出现频率
AsyncTask<std::vector<std::pair<char, size_t>>> analyze_file(const std::string& path) {
std::string content = co_await async_read_file(path);
if (content.empty()) {
co_return {}; // 返回空结果
}
std::vector<std::pair<char, size_t>> freq(256, {0, 0});
for (char ch : content) {
freq[static_cast<unsigned char>(ch)].second++;
}
// 过滤只保留出现过的字符
std::vector<std::pair<char, size_t>> result;
std::copy_if(freq.begin(), freq.end(), std::back_inserter(result),
[](const auto& p){ return p.second > 0; });
// 按出现次数降序排序
std::sort(result.begin(), result.end(),
[](const auto& a, const auto& b){ return a.second > b.second; });
co_return result;
}
// 简易主函数,演示协程调用
int main() {
std::string filename = "sample.txt";
auto future = analyze_file(filename);
auto freq = future.get(); // 阻塞等待协程完成
std::cout << "字符出现频率统计(前10个):\n";
for (size_t i = 0; i < std::min<size_t>(10, freq.size()); ++i) {
std::cout << static_cast<int>(freq[i].first) << " -> " << freq[i].second << "\n";
}
return 0;
}
4. 关键实现点说明
AsyncTask包装器:为协程提供std::future返回值,简化异步调用。co_await std::suspend_always{}:使协程在入口处挂起,从而让后续代码在新的线程池或异步上下文中执行,避免阻塞主线程。- 错误处理:
async_read_file捕获异常并返回空字符串,analyze_file通过检查空内容来决定是否继续处理。 - 性能优化:一次性读取文件到字符串后再做统计,避免多次磁盘 I/O。
5. 扩展思路
- 多文件并行:可以将多个
async_read_file协程放进std::when_all中,批量读取并行处理。 - 流式读取:改为使用
co_yield逐块读取文件内容,适用于大文件。 - 异步网络:结合
asio或boost::asio实现网络请求的异步处理,保持协程风格。
6. 结语
通过上述示例,我们可以看到C++20协程为异步文件处理提供了更简洁、更安全的语法。只需要几行代码,即可完成后台读取、错误捕获和数据分析,显著提升代码可读性与执行效率。希望本文能为你在项目中使用协程提供实用参考。