**利用 C++20 Coroutines 编写简易异步文件读取器**

C++20 引入的协程(Coroutines)为异步编程带来了极大的便利。它们可以让我们像编写同步代码那样写异步逻辑,隐藏了复杂的状态机实现。下面我们用一个小示例来演示如何利用协程读取文件内容,并把读取结果返回给调用者。


1. 协程的基本概念

协程的核心是一个 promise 对象,它保存协程的状态,并定义协程的入口、挂起点以及结束点。C++ 标准库提供了 std::suspend_alwaysstd::suspend_never 等简易挂起策略,结合 co_awaitco_yieldco_return,就能实现异步流程。


2. 设计思路

  • 异步读取:我们把文件读取封装成一个 async_read 协程,读取一个文件块后 co_await 一个 I/O 事件,完成后返回读取到的字节数。
  • 任务包装:使用 std::future 来包装协程的最终结果,方便与普通同步代码交互。
  • 简易 I/O 事件:由于标准库暂不直接提供事件循环,我们在示例中使用 std::async 作为异步 I/O 的占位实现,真正项目中可替换为 libuv、asio 等事件驱动框架。

3. 示例代码

#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <coroutine>
#include <vector>
#include <thread>
#include <chrono>

// 简单的异步读取事件占位,实际项目请使用 libuv/Asio 等
struct AsyncReadEvent {
    struct promise_type {
        std::future<std::size_t> get_return_object() {
            return std::future<std::size_t>(std::move(result_promise.get_future()));
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(std::size_t val) { result_promise.set_value(val); }
        void unhandled_exception() { result_promise.set_exception(std::current_exception()); }

        std::promise<std::size_t> result_promise;
    };
};

using awaitable_size_t = std::future<std::size_t>;

awaitable_size_t async_read_chunk(std::ifstream &ifs, char *buffer, std::size_t size) {
    // 这里用 std::async 模拟异步 I/O
    return std::async(std::launch::async, [&ifs, buffer, size]() {
        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟 I/O 延迟
        ifs.read(buffer, size);
        return static_cast<std::size_t>(ifs.gcount());
    });
}

struct FileReader {
    std::ifstream ifs;
    std::size_t chunk_size;

    FileReader(const std::string &path, std::size_t chunk = 1024)
        : ifs(path, std::ios::binary), chunk_size(chunk) {}

    // 协程函数,返回 std::future<std::vector<char>>,包含完整文件内容
    std::future<std::vector<char>> read_all() {
        std::vector <char> result;
        while (ifs) {
            std::vector <char> buffer(chunk_size);
            auto size_future = async_read_chunk(ifs, buffer.data(), buffer.size());
            std::size_t n = co_await size_future; // 等待 I/O 完成
            if (n > 0) {
                result.insert(result.end(), buffer.begin(), buffer.begin() + n);
            }
        }
        co_return result; // 传回完整文件
    }
};

int main() {
    FileReader reader("example.txt", 512);
    auto future = reader.read_all(); // 启动协程
    std::vector <char> data = future.get(); // 阻塞等待结果

    std::cout << "读取文件共 " << data.size() << " 字节。\n";
    std::cout << "内容预览:\n" << std::string(data.begin(), data.end()).substr(0, 100) << "...\n";
    return 0;
}

4. 关键点剖析

  1. async_read_chunk
    通过 std::async 模拟异步 I/O。协程在 co_await 时会挂起,等到 async 任务完成后恢复执行。

  2. 协程返回 std::future
    std::future 让协程的结果可以像普通异步操作一样被等待。若想在事件循环中直接挂起而不阻塞,可以结合自定义事件循环,将 awaitable_size_t 换成与循环兼容的 awaitable。

  3. 错误处理
    promise_type::unhandled_exception 会捕获异常并传递给 future,调用方可以通过 future.get() 捕获异常或检查 future.wait_for 的状态。

  4. 可扩展性

    • 可以把 async_read_chunk 替换为真正的异步文件 I/O,例如使用 boost::asio::async_read
    • 对于大文件,建议使用 std::shared_ptr<std::vector<char>>std::unique_ptr 以避免拷贝。
    • 结合 std::generator(C++23)可以实现更细粒度的流式读取。

5. 结语

C++20 的协程为 I/O 密集型应用提供了新的思路。通过把异步读取包装成协程,我们既保留了直观的代码风格,又能充分利用异步事件循环的性能。希望这个小示例能为你在项目中使用协程提供参考。祝编码愉快!

发表评论