C++20 协程:实现异步 I/O 的完整流程

在 C++20 中,协程(coroutines)被正式纳入标准库,成为一种强大的异步编程工具。通过协程可以以同步代码的语法编写异步逻辑,极大提升代码可读性与可维护性。下面我们以实现一个简单的异步文件读取为例,系统阐述从协程定义、状态机实现到异步 I/O 的完整流程。

1. 协程的基本概念

协程是支持挂起(co_await)与恢复(co_returnco_yield)的函数。其执行过程被分为若干 suspend points(挂起点),在这些点上协程可以将控制权交还给调用者,随后根据需要恢复执行。协程的实现核心是 状态机:编译器会把协程代码拆分为若干状态块,生成一个隐式的 promise_type,并在其中维护协程的运行状态。

2. 协程的返回类型

C++20 标准库提供了几种协程返回类型,最常用的有:

  • `std::future `:与 std::async 配合使用,支持同步等待结果。
  • `std::generator `:支持 `co_yield`,可生成序列。
  • `std::task `(在 “ 中):更轻量的协程包装,适合自定义 I/O 逻辑。

下面我们使用 std::future<std::string>,并配合 std::experimental::generator 用于读取文件行。

3. 异步 I/O 的实现思路

在标准 C++17 之前,异步 I/O 需要依赖第三方库(如 Boost.Asio、libuv)。C++20 协程本身并不提供 I/O 接口,但可以与 事件循环 结合,例如:

  1. 使用 std::async 或自定义线程池包装底层同步 I/O,转为异步。
  2. 使用第三方事件循环(如 libuvasio)的异步接口,配合协程挂起。

这里演示一个简化的例子:通过 std::async 读取文件,然后用 co_await 等待完成。

4. 代码实现

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

namespace coro = std::experimental;

// ① 读取单行的异步协程
coro::generator<std::string> asyncReadLines(const std::string& path) {
    // 创建一个同步任务来读取文件
    auto readTask = std::async(std::launch::async, [&path]() {
        std::ifstream file(path, std::ios::binary);
        if (!file) return std::vector<std::string>{};

        std::vector<std::string> lines;
        std::string line;
        while (std::getline(file, line)) {
            lines.push_back(line);
        }
        return lines;
    });

    // 等待异步任务完成
    co_await std::experimental::suspend_always{}; // 这里可替换为真正的 I/O 事件挂起

    // 获取结果
    std::vector<std::string> lines = readTask.get();
    for (const auto& l : lines) {
        co_yield l; // 逐行产生
    }
}

// ② 主程序:消费协程
int main() {
    std::string filePath = "sample.txt";

    std::cout << "开始异步读取文件:" << filePath << "\n";

    for (const auto& line : asyncReadLines(filePath)) {
        std::cout << line << "\n";
    }

    std::cout << "文件读取完毕。\n";
    return 0;
}

说明

  1. asyncReadLines:使用 std::async 创建后台线程读取文件,返回一个 std::vector<std::string>。随后在协程中 co_await 一个 suspend_always(示例占位符,实际使用中可以改为真正的 I/O 事件挂起)。完成后遍历行并通过 co_yield 逐行返回。

  2. 事件循环:如果你使用的是 asiolibuv,可以把 co_await 替换为 co_await asio::async_read_until 或相似接口,挂起当前协程,等待 I/O 完成后恢复。

  3. 返回类型:这里使用 coro::generator<std::string>,适合需要按序生成结果的场景。若只想一次性获得完整结果,可直接返回 std::future<std::string>

5. 性能与优化

  • 减少拷贝std::async 创建的线程会把文件内容复制到内存,若文件巨大可考虑使用内存映射(mmap)或按块读取。
  • 事件驱动:在高性能服务器中,建议使用事件循环与协程结合,避免每次 I/O 都创建线程。asio::awaitableco_await 可直接挂起在 OS 的事件上。
  • 异常处理:协程支持 try/catch,在 co_returnco_yield 前捕获异常,并通过 promise_typeunhandled_exception 传递。

6. 结语

C++20 协程为异步编程提供了更直观的语法,配合现代 I/O 框架即可实现高并发、低延迟的异步服务。虽然标准库本身尚未内置 I/O 事件循环,但通过与第三方库结合,你可以轻松构建可维护且性能优秀的异步 C++ 应用。希望本文的示例能为你上手协程与异步 I/O 打下基础。

发表评论