如何使用C++20协程实现异步文件读取?

在C++20中,协程(coroutine)被正式纳入标准库,为异步编程提供了更简洁、更直观的语法。本文将演示如何利用C++20协程实现一个异步文件读取器,并解释其工作原理与关键点。

1. 先决条件

  • 编译器支持C++20协程:如 GCC 11+, Clang 13+, MSVC 19.29+。
  • 标准库包含 ` `,文件 I/O 用 “。

2. 设计思路

我们需要一个能返回 std::future 或自定义 Awaitable 的协程。下面的实现采用 std::futurestd::promise 的组合,完成:

  1. 异步读取器async_read_file(const std::string& path),返回 std::future<std::string>
  2. 协程主体:使用 co_await 等待 I/O 完成后,将结果传递给 co_return

虽然标准库并未提供真正的异步文件 I/O,下面的示例通过 std::async 模拟后台线程完成 I/O,然后协程等待其结果。

3. 代码实现

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

// ---------- 简易 Awaitable ----------
struct AsyncFileAwaitable {
    std::future<std::string> fut;
    explicit AsyncFileAwaitable(std::future<std::string> f) : fut(std::move(f)) {}

    bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
    void await_suspend(std::coroutine_handle<> h) {
        // 当协程挂起时,启动后台任务
        std::thread([h, f = std::move(fut)]() mutable {
            // 让后台线程等待 I/O 完成
            f.wait();
            // I/O 完成后恢复协程
            h.resume();
        }).detach();
    }
    std::string await_resume() { return fut.get(); }
};

// ---------- 异步读取函数 ----------
AsyncFileAwaitable async_read_file(const std::string& path) {
    // 通过 std::async 模拟后台 I/O
    auto fut = std::async(std::launch::async, [path]() -> std::string {
        std::ifstream file(path, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot open file: " + path);
        std::string content((std::istreambuf_iterator <char>(file)),
                            std::istreambuf_iterator <char>());
        return content;
    });
    return AsyncFileAwaitable(std::move(fut));
}

// ---------- 协程入口 ----------
struct AsyncFileReader {
    struct promise_type {
        AsyncFileReader get_return_object() { return {}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

void read_file_task(const std::string& path) {
    AsyncFileReader coro; // 协程句柄占位
    std::string content = co_await async_read_file(path);
    std::cout << "文件内容(长度 " << content.size() << " 字节)已读取。\n";
    // 进一步处理 content ...
}

// ---------- 主程序 ----------
int main() {
    std::string file_path = "example.txt";
    try {
        read_file_task(file_path);  // 启动协程
    } catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << '\n';
    }
    // 为了让后台线程有时间完成,简单延迟
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

4. 关键点说明

  1. Awaitable 结构

    • await_ready 判断是否已完成。
    • await_suspend 将协程挂起,并在后台线程完成 I/O 后恢复协程。
    • await_resume 在协程恢复时获取结果。
  2. async_read_file

    • 利用 std::async 将同步文件读取包装为异步任务。
    • 返回自定义 AsyncFileAwaitable,实现与协程的无缝交互。
  3. 协程入口

    • 由于我们只需要演示异步读取,AsyncFileReader 仅提供 promise_type 的最小实现。
    • read_file_task 中使用 co_await 等待文件读取完成。
  4. 后台线程与协程的配合

    • await_suspend 通过 std::thread 启动后台工作。
    • detach() 保证后台线程不阻塞主线程。
    • 当 I/O 结束后 h.resume() 恢复协程。

5. 性能与实际应用

  • 该实现仅为演示;在真实项目中,应使用专门的异步 I/O 库(如 Boost.Asio、libuv 等)或操作系统原生异步 API(如 Linux 的 io_uring)。
  • C++20 协程仅提供语言层面的语法支持,具体异步机制仍需库或系统支持。
  • 若目标是高并发文件读取,建议将 I/O 与协程结合使用,并配合线程池或事件循环。

6. 小结

本文通过 async_read_file 与自定义 Awaitable,展示了如何在 C++20 协程中实现异步文件读取。虽然使用了 std::async 模拟后台线程,但核心思路可以迁移到真正的异步 I/O 框架中,进一步提升性能与可扩展性。掌握这一模式后,你可以轻松在 C++20 项目中加入高效的异步 I/O 逻辑。

发表评论