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

一、背景与意义

在传统的 C++ 中,异步 I/O 需要借助线程、回调或第三方库(如 Boost.Asio、libuv 等)。这些方案虽然成熟,但代码往往繁琐、难以维护。C++20 引入了 协程(coroutine),为编写异步逻辑提供了更直观的语言级支持。通过 co_awaitco_return 等关键字,开发者可以像同步代码一样书写异步逻辑,从而显著降低复杂度。

本文将以 异步文件读取 为例,演示如何使用 C++20 协程实现一个简洁、可复用的异步读文件 API,并对其内部机制做简要说明。

二、协程基础

在 C++20 中,协程的核心是一个 promise type(承诺类型)与 future type(期货类型)的配合。简化步骤如下:

  1. 编写 promise type:实现 get_return_object(), initial_suspend(), final_suspend(), return_value(), unhandled_exception() 等成员函数。
  2. 编写 awaitable type:实现 await_ready(), await_suspend(), await_resume()
  3. 调用协程:使用 co_awaitco_return,得到的对象即为未来的值。

标准库中已提供 std::future, std::promise, std::async 等,但这些都不支持协程。我们可以借助 std::experimental::generator 或第三方库 cppcoroasio::awaitable 等实现更通用的协程接口。为简化演示,本文自行实现一个轻量级 async_task 类型。

三、实现步骤

1. 定义 async_task

#include <coroutine>
#include <exception>
#include <iostream>
#include <vector>
#include <cstring>   // for std::memcpy

// 简易的异步任务包装器
template<typename T>
class async_task {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    async_task(handle_type h) : coro(h) {}
    async_task(const async_task&) = delete;
    async_task(async_task&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
    ~async_task() { if (coro) coro.destroy(); }

    T get() {
        if (!coro.done()) coro.resume();
        if (coro.promise().exception) std::rethrow_exception(coro.promise().exception);
        return std::move(coro.promise().value);
    }

    struct promise_type {
        T value;
        std::exception_ptr exception;

        auto get_return_object() {
            return async_task{handle_type::from_promise(*this)};
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exception = std::current_exception(); }
        void return_value(T val) { value = std::move(val); }
    };

private:
    handle_type coro;
};

说明

  • promise_type 保存返回值与异常。
  • get() 方法阻塞等待协程完成,简化使用。
  • 这里使用 std::suspend_never 表示协程立即开始执行,std::suspend_always 表示在 final_suspend 时暂停,让外部释放资源。

2. 定义 awaitable 类型:file_read_op

#include <filesystem>
#include <fcntl.h>      // open
#include <unistd.h>     // read, close
#include <system_error>

struct file_read_op {
    int fd;
    std::size_t size;
    char* buffer;

    file_read_op(int fd, std::size_t size, char* buffer)
        : fd(fd), size(size), buffer(buffer) {}

    bool await_ready() noexcept { return false; }

    // 将协程挂起,并在后台线程完成 I/O 后唤醒
    std::suspend_always await_suspend(std::coroutine_handle<> h) noexcept {
        // 简单示例:在当前线程同步完成 I/O 并唤醒
        // 实际应用可改为线程池或异步 I/O
        ssize_t n = ::read(fd, buffer, size);
        if (n < 0) {
            // 这里我们直接把错误信息写入 promise,省略细节
            h.promise().exception = std::make_exception_ptr(
                std::system_error(errno, std::generic_category(), "read error"));
        } else {
            h.promise().value = static_cast<std::size_t>(n);
        }
        return {}; // 立即唤醒
    }

    std::size_t await_resume() noexcept { return 0; } // 结果已写入 promise
};

说明

  • await_suspend 在这里直接执行 read(),但在真正的异步环境下,你可以把 I/O 操作交给线程池或事件循环。
  • 我们把读取到的字节数写入协程的 promise 对象,使 async_taskget() 可以获取结果。

3. 组合协程函数

async_task<std::size_t> async_read_file(const std::string& path, std::vector<char>& out) {
    int fd = ::open(path.c_str(), O_RDONLY);
    if (fd < 0) {
        co_return static_cast<std::size_t>(0);
    }

    // 先获取文件大小
    std::filesystem::path p(path);
    std::size_t file_size = std::filesystem::file_size(p);
    out.resize(file_size);

    // 调用 awaitable
    co_await file_read_op(fd, file_size, out.data());

    ::close(fd);
    co_return file_size;
}

说明

  • 这里把文件大小读取、缓冲区分配、协程等待等逻辑串联。
  • co_await 会挂起当前协程,直到 await_suspend 完成。

四、使用示例

int main() {
    std::vector <char> data;
    async_task<std::size_t> task = async_read_file("example.txt", data);

    // 可以在此处执行其他同步操作
    std::cout << "正在读取文件...\n";

    std::size_t bytes = task.get(); // 阻塞等待完成
    std::cout << "读取完成,字节数:" << bytes << '\n';

    // 输出文件内容
    std::cout.write(data.data(), bytes);
    std::cout << '\n';
}

五、性能与可扩展性

  1. 性能

    • 协程本身几乎无运行时开销;真正的 I/O 仍是阻塞 read()
    • 若改为事件驱动的异步 I/O(如 io_uringlibuv),只需在 await_suspend 中注册回调,协程仍然保持简洁。
  2. 可扩展性

    • async_task 可进一步包装为 async_task<std::vector<char>>,返回完整缓冲区。
    • 对于多文件并行读取,可使用 std::vector<async_task<std::size_t>> 并调用 co_awaitstd::when_all(第三方库提供)等待全部完成。
  3. 错误处理

    • await_suspend 中捕获异常并存入 promiseget() 会重新抛出。
    • 对于 I/O 超时或取消等高级场景,可在 await_suspend 中使用 std::condition_variable 或事件循环的取消机制。

六、总结

本文展示了如何利用 C++20 协程实现一个简洁的异步文件读取 API。通过自定义 async_taskawaitable,我们在保持代码可读性的同时,仍能充分利用系统的异步 I/O 机制。随着 C++20 的普及,协程将成为处理 I/O 密集型任务的首选手段,为高性能、低耦合的应用程序奠定基础。

发表评论