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

在C++20中,协程(coroutine)被引入为语言级特性,提供了一种轻量级的异步编程模型。相比传统的线程或回调机制,协程可以让我们用同步风格的代码编写异步逻辑,代码可读性更高,错误更少。下面以实现一个异步文件读取器为例,展示如何使用C++20协程完成从磁盘读取文件并返回内容的功能。

1. 预备知识

  • co_await:挂起协程,等待一个“可等待”对象完成。
  • co_return:返回协程的结果。
  • std::suspend_always / std::suspend_never:用于控制协程的挂起/恢复行为。
  • promise_type:协程的承诺类型,决定协程的行为和返回值。

2. 设计思路

  1. 可等待对象:我们需要一个可等待的包装器,用来封装异步 I/O 操作。这里使用 std::futurestd::async 的组合来模拟异步读取。
  2. 协程返回类型:定义一个 `Task ` 模板类,表示一个返回类型为 `T` 的协程任务。它内部会持有 `std::future`。
  3. 异步读取函数async_read_file 接受文件路径,返回 Task<std::string>。在协程内部,使用 co_await 等待 std::future 的完成,并将结果返回。

3. 代码实现

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

// 1. Task <T>:协程任务封装
template<typename T>
struct Task {
    struct promise_type {
        std::future <T> fut;
        T result; // 只在需要返回值时使用

        // 进入协程时返回的句柄
        auto get_return_object() { return Task{std::move(fut)}; }

        // 协程开始时不挂起
        std::suspend_never initial_suspend() noexcept { return {}; }

        // 协程结束时挂起,等待外部获取结果
        std::suspend_always final_suspend() noexcept { return {}; }

        // 处理异常
        void unhandled_exception() { std::terminate(); }

        // 返回值
        void return_value(T v) {
            result = std::move(v);
            // 将结果包装到 future
            fut = std::async(std::launch::deferred, [r=std::move(result)]{ return r; });
        }
    };

    std::future <T> fut; // 持有协程的 future

    // 阻塞等待结果
    T get() { return fut.get(); }
};

// 2. 可等待对象:包装 std::future
struct AwaitableFuture {
    std::future<std::string> fut;

    bool await_ready() { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }

    void await_suspend(std::coroutine_handle<> h) {
        // 在后台线程完成后唤醒协程
        std::thread([h, f=&fut]() mutable {
            f->wait();
            h.resume();
        }).detach();
    }

    std::string await_resume() { return fut.get(); }
};

// 3. 异步文件读取
Task<std::string> async_read_file(const std::string& path) {
    // 在后台线程读取文件
    std::future<std::string> fileFuture = std::async(std::launch::async, [path]() {
        std::ifstream ifs(path, std::ios::binary);
        if (!ifs) throw std::runtime_error("Cannot open file");

        std::string content((std::istreambuf_iterator <char>(ifs)),
                            std::istreambuf_iterator <char>());
        return content;
    });

    AwaitableFuture awaitable{ std::move(fileFuture) };

    // 挂起,等待文件读取完成
    std::string data = co_await awaitable;

    co_return data;
}

// 4. 主程序
int main() {
    try {
        auto task = async_read_file("example.txt");

        // 这里可以做其他工作
        std::cout << "Doing other work while file is loading...\n";

        // 等待结果
        std::string content = task.get();

        std::cout << "File content (" << content.size() << " bytes):\n";
        std::cout << content.substr(0, 200) << "...\n";
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << '\n';
    }
    return 0;
}

代码说明

  • `Task `:包装协程的返回值,通过 `co_return` 将结果放入 `std::future`,外部可通过 `get()` 获取。
  • AwaitableFuture:将 std::future<std::string> 变为可等待对象。await_suspend 在后台线程完成后恢复协程,避免阻塞主线程。
  • async_read_file:启动异步读取,挂起协程直到读取完成,然后返回字符串内容。

4. 性能与可扩展性

  • IO 调度:上述示例使用 std::async,内部实现依赖实现细节,可能使用线程池或单线程 I/O。生产环境可使用更高效的异步 I/O(如 io_uring、Boost.Asio 的异步文件 I/O)。
  • 错误处理:协程内部若抛出异常,unhandled_exception 会直接终止程序。可以在 promise_type 中自定义 unhandled_exception,将异常包装进 std::future,让调用方通过 get() 捕获。
  • 多文件并行:可在同一线程内启动多个 async_read_file,并在主循环中 co_await 所有结果,利用协程调度实现高并发 I/O。

5. 结语

C++20 的协程为异步编程带来了新的语法糖,使得原本需要回调链的异步 I/O 能以同步代码的直观写法实现。通过以上示例,你可以快速搭建一个基于协程的异步文件读取器,并在此基础上扩展到网络、数据库等多种异步任务。祝你编码愉快!

发表评论