**C++20协程实现异步文件读取**

在现代 C++ 开发中,异步 I/O 已成为高性能网络服务和文件处理的核心技术之一。C++20 引入的协程(coroutines)为实现高效、可读的异步逻辑提供了强大工具。本文将通过一个完整示例,演示如何利用 C++20 协程完成文件的异步读取,并在读取过程中实现分块处理、错误捕获以及资源自动释放。


1. 目标功能

  • 异步读取:不阻塞主线程,读取完成后再通知业务层。
  • 分块读取:一次读取固定大小块,便于对大文件流式处理。
  • 错误处理:捕获文件打开、读取错误并返回异常信息。
  • 资源管理:使用 RAII 自动关闭文件句柄。

2. 关键技术点

技术 说明
std::experimental::generator 用于生成可迭代的协程数据流,C++20 中已标准化为 std::generator.
std::future/std::promise 协程与调用方交互的标准方式。
std::ifstream C++ 标准文件流,配合 std::ios::binary 打开。
std::error_code 统一错误表示。

3. 代码实现

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <future>
#include <exception>
#include <filesystem>
#include <coroutine>

// 简易协程生成器(C++20 std::generator 已包含)
template <typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::exception_ptr eptr;

        generator get_return_object() {
            return generator{
                std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }
        void return_void() {}
        void unhandled_exception() { eptr = std::current_exception(); }
    };

    std::coroutine_handle <promise_type> coro;
    generator(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }

    struct iterator {
        std::coroutine_handle <promise_type> coro;
        bool done = false;

        iterator(std::coroutine_handle <promise_type> h, bool d)
            : coro(h), done(d) {}

        iterator& operator++() {
            coro.resume();
            done = coro.done();
            return *this;
        }

        T operator*() const { return coro.promise().current_value; }
        bool operator==(const iterator& other) const { return done == other.done; }
        bool operator!=(const iterator& other) const { return !(*this == other); }
    };

    iterator begin() {
        coro.resume();
        return iterator{coro, coro.done()};
    }
    iterator end() { return iterator{coro, true}; }
};

// 异步读取块
generator<std::vector<char>> async_read_blocks(const std::string& path,
                                               std::size_t block_size = 4096) {
    std::ifstream file(path, std::ios::binary);
    if (!file) {
        throw std::runtime_error("无法打开文件: " + path);
    }

    while (file) {
        std::vector <char> buffer(block_size);
        file.read(buffer.data(), static_cast<std::streamsize>(block_size));
        std::streamsize bytes = file.gcount();
        if (bytes > 0) {
            buffer.resize(static_cast<std::size_t>(bytes));
            co_yield buffer;
        }
    }
    co_return;
}

// 主程序:使用 std::future 触发协程
std::future<std::size_t> read_file_async(const std::string& path) {
    return std::async(std::launch::async, [path] {
        std::size_t total_bytes = 0;
        try {
            for (auto&& block : async_read_blocks(path)) {
                // 模拟处理:这里简单累加字节数
                total_bytes += block.size();
                // 你可以在此处加入更复杂的业务逻辑
            }
        } catch (const std::exception& e) {
            std::cerr << "读取错误: " << e.what() << '\n';
            throw; // 重新抛出,future 会携带异常
        }
        return total_bytes;
    });
}

// 示例:读取指定文件并打印总字节数
int main() {
    std::string filename = "sample.txt";
    try {
        auto fut = read_file_async(filename);
        std::size_t size = fut.get(); // 阻塞直到完成
        std::cout << "文件 '" << filename << "' 共计 " << size << " 字节\n";
    } catch (const std::exception& e) {
        std::cerr << "异常: " << e.what() << '\n';
    }
    return 0;
}

4. 代码说明

  1. generator

    • 自定义的协程生成器,用于 co_yield 数据块。
    • 通过 iterator 与标准循环语法配合,实现 for(auto&& block : async_read_blocks(...))
  2. async_read_blocks

    • 打开文件并逐块读取。
    • 读取到的数据直接通过 co_yield 暴露给调用方。
    • 在文件读完后自动销毁协程,释放文件句柄。
  3. read_file_async

    • 使用 std::async 将协程包装为 std::future
    • 这样主线程不会被阻塞,业务层可以通过 future.get()future.wait() 等方式等待结果。
    • 异常在协程内部捕获后重新抛出,future 会携带异常信息。
  4. main

    • 调用 read_file_async 并等待结果。
    • 打印读取的总字节数;若出现错误则输出错误信息。

5. 优点与扩展

  • 可读性:协程使异步流程像同步代码一样直观。
  • 高效:只在需要时才产生新的块,避免一次性读入整个文件。
  • 可扩展:可将 co_yield 替换为网络 I/O、数据库查询等异步源。
  • 错误处理:统一捕获并返回,易于调试。

若想进一步优化:

  • 使用 std::filesystem 检查文件大小并预估块数。
  • 将块大小设为可配置,根据实际磁盘 I/O 性能动态调整。
  • 结合 std::asyncstd::future 的链式调用,实现多阶段异步管道。

总结
C++20 协程为异步文件读取提供了极简、可维护的实现方式。通过本示例,你可以快速在项目中集成异步 I/O,并在此基础上构建更复杂的异步处理流水线。

发表评论