C++20中协程的实战案例:异步文件读取

在C++20中,协程(coroutines)被正式纳入标准库,提供了更直观、更高效的异步编程方式。下面我们通过一个完整的示例,演示如何使用协程实现异步文件读取,并利用std::filesystemstd::async来完成高性能的文件处理。

1. 关键概念回顾

  • 协程函数:返回std::futurestd::generator或自定义类型,内部使用co_awaitco_yieldco_return实现挂起与恢复。
  • std::filesystem:用于跨平台文件系统操作,读取文件大小、遍历目录等。
  • std::async:在后台线程中执行耗时任务,结合协程可实现真正的异步。

2. 设计思路

  1. 定义一个异步读取器async_read_file(const std::string& path) 返回std::future<std::string>,在后台线程读取文件内容。
  2. 主协程:使用co_await等待文件读取完成,然后对内容进行进一步处理(如字符串统计、哈希计算等)。
  3. 错误处理:使用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逐块读取文件内容,适用于大文件。
  • 异步网络:结合asioboost::asio实现网络请求的异步处理,保持协程风格。

6. 结语

通过上述示例,我们可以看到C++20协程为异步文件处理提供了更简洁、更安全的语法。只需要几行代码,即可完成后台读取、错误捕获和数据分析,显著提升代码可读性与执行效率。希望本文能为你在项目中使用协程提供实用参考。

发表评论