利用C++20的协程实现异步文件读取

在传统的C++编程中,文件读取往往是同步阻塞的操作,尤其在需要高并发或I/O密集型的场景下,阻塞会导致线程资源被浪费。随着C++20标准引入协程(coroutine)这一语言特性,我们可以在保持代码可读性和结构化的同时,实现真正的异步文件读取。下面我们通过一个完整的示例,展示如何使用协程结合std::experimental::filesystemstd::future来完成异步文件读取。

1. 关键概念回顾

  • 协程:一种轻量级的线程,允许在函数执行中间暂停,并在需要时恢复。协程的实现由编译器完成,使用co_awaitco_return等关键字。
  • std::experimental::coroutine_handle:协程句柄,用于控制协程的生命周期。
  • std::experimental::generator:一种返回序列的协程,用于逐步产生值。

2. 设计思路

我们希望:

  1. 读取指定文件路径。
  2. 将文件内容逐块(如每块4KB)读取到缓冲区。
  3. 每读取完一块后,异步返回该块数据供调用方处理。
  4. 在主线程中,使用co_await等待每块数据并写入到输出流或进行其他处理。

为了实现这一点,先创建一个协程生成器 async_read_file_block,返回 std::future<std::string>,每次 co_await 时产生一块内容。

3. 代码实现

#include <iostream>
#include <fstream>
#include <vector>
#include <future>
#include <experimental/coroutine>
#include <experimental/generator>
#include <string>

namespace stdex = std::experimental;

// 生成器返回每块文件内容
struct async_file_reader {
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle <promise_type>;

    struct promise_type {
        stdex::generator<std::string> get_return_object() {
            return stdex::generator<std::string>::from_promise(*this);
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };

    using generator_type = stdex::generator<std::string>;
    generator_type gen;
};

async_file_reader async_read_file_block(const std::string& path, std::size_t block_size = 4096) {
    std::ifstream fin(path, std::ios::binary);
    if (!fin) {
        throw std::runtime_error("Cannot open file");
    }

    std::vector <char> buffer(block_size);
    while (fin.read(buffer.data(), block_size) || fin.gcount() > 0) {
        std::string block(buffer.data(), fin.gcount());
        co_yield block;
    }
}

int main() {
    try {
        const std::string file_path = "sample.dat";

        // 启动协程
        auto reader = async_read_file_block(file_path);

        // 逐块处理
        for (auto&& block : reader.gen) {
            // 这里以打印块大小为例
            std::cout << "Read block of size: " << block.size() << " bytes\n";
            // 你可以在此处把块写入磁盘或发送到网络等
        }

        std::cout << "File read complete.\n";
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

4. 关键点说明

  1. 协程生成器async_read_file_block 返回一个 async_file_reader,内部使用 co_yield 产生每块数据。C++20 标准库中的 generator 使得协程能像普通迭代器一样使用 for 循环。
  2. 块读取:使用 ifstream::readgcount 组合读取可变大小块,避免最后一块不足 block_size 的情况。
  3. 错误处理:协程内部使用 throw 抛出异常,外层捕获处理。
  4. 可扩展性:可以将 block 的处理逻辑替换为异步写文件、网络传输或数据压缩等,保持代码结构清晰。

5. 进一步优化

  • 使用 std::async:如果想把读取操作与主线程完全解耦,可以让协程内部调用 std::async 并返回 std::future<std::string>,在主线程中 co_await
  • 内存映射:对于极大文件,考虑使用 mmapboost::interprocess 等技术,以减少复制开销。
  • 并发读取:将文件拆分成多个区块,分别由多个协程读取并合并,利用多核 CPU 提升吞吐量。

6. 结语

C++20 的协程为 I/O 密集型任务提供了更优雅、更直观的编程方式。通过上述示例,你可以看到协程如何在保持代码可读性的同时,实现真正的异步文件读取。随着标准库的进一步完善和社区生态的发展,协程将在 C++ 的高性能网络、文件系统乃至游戏开发等领域发挥越来越重要的作用。

发表评论