## C++20 协程:用协程实现异步 I/O 的完整示例与最佳实践

一、引言

自 C++20 起,标准库正式加入协程(coroutine)支持,标志着 C++ 语言进入了异步编程的新时代。协程本质上是可以被挂起与恢复的函数,它让“写同步、运行异步”的编程模式变得简单自然。本文从基本概念出发,结合完整的 std::asyncstd::future 与自定义 awaitable 对象,演示如何在 C++ 中使用协程完成高效的异步 I/O,并给出常见问题与优化技巧。

二、协程的核心组成

  1. co_await:挂起当前协程并等待 awaitable 对象完成。
  2. co_yield:在生成器模式中产生一个值并挂起协程。
  3. co_return:结束协程并返回一个值。
  4. promise_type:协程内部实现的桥梁,管理协程状态与返回值。
  5. awaiter:实现 await_ready()await_suspend()await_resume() 的对象。

三、实现一个异步文件读取协程

下面给出一个完整的例子,演示如何使用 std::filesystemstd::async 结合协程实现异步文件读取。

#include <coroutine>
#include <future>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>

// awaitable 对象:包装 std::future
template<typename T>
struct AsyncAwaitable {
    std::future <T> fut;
    AsyncAwaitable(std::future <T> 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([this, h]() mutable {
            fut.wait();          // 阻塞等待
            h.resume();          // 继续协程
        }).detach();
    }

    T await_resume() { return fut.get(); }
};

// 协程函数:异步读取文件
std::future<std::string> async_read_file(const std::filesystem::path& path) {
    return std::async(std::launch::async, [&path]() {
        std::ifstream file(path, std::ios::binary);
        if (!file) return std::string("文件打开失败");
        std::string content((std::istreambuf_iterator <char>(file)),
                             std::istreambuf_iterator <char>());
        return content;
    });
}

// 使用协程的异步主函数
struct AsyncMain {
    struct promise_type {
        AsyncMain get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };
};

AsyncMain async_main() {
    std::filesystem::path p = "example.txt";
    std::string data = co_await AsyncAwaitable<std::string>(async_read_file(p));
    std::cout << "文件内容长度: " << data.size() << " 字节\n";
    std::cout << "内容前 100 字节:\n" << data.substr(0, 100) << "\n";
}

关键点说明

  • AsyncAwaitable:包装 std::future 并实现协程挂起与恢复。
  • async_read_file:使用 std::async 创建后台线程读取文件。
  • async_main:示例协程,展示如何 co_await 异步 I/O 并处理结果。

四、协程与传统线程池的对比

维度 传统线程池 C++ 协程
资源占用 每个任务需要完整线程(大约 1MB 堆栈) 协程仅占用几 KB 的栈帧,挂起后不占用线程
调度模型 线程调度器决定 协程调度交给程序,易于实现自定义事件循环
错误传播 通过异常或回调 通过 promise_type::unhandled_exception 统一异常传播
可读性 需要回调或 std::future co_await 让异步代码像同步一样线性

五、常见陷阱与解决方案

  1. 未使用 std::launch::async

    • std::async 默认行为为 std::launch::deferred,可能导致同步阻塞。
    • 解决:显式指定 std::launch::async
  2. 未正确管理协程生命周期

    • 协程句柄在 await_suspend 结束后若未 resume(),协程会悬挂。
    • 解决:在 awaiter 的 await_suspend 中手动 h.resume()
  3. 异常传播失效

    • 如果在 awaiter 的 await_resume() 抛异常,需在 promise_type::unhandled_exception() 中处理。
    • 解决:实现自定义异常处理逻辑,或在调用方使用 try/catch 包裹。
  4. 过度使用协程导致堆栈溢出

    • 递归协程如果深度过大仍会耗尽栈。
    • 解决:将深层递归改为循环或使用尾递归优化。

六、进阶主题:自定义事件循环

在大规模网络服务器中,通常需要一个事件循环(Event Loop)来驱动协程。示例伪代码:

struct Scheduler {
    std::deque<std::coroutine_handle<>> ready;
    void schedule(std::coroutine_handle<> h) { ready.push_back(h); }
    void run() {
        while (!ready.empty()) {
            auto h = ready.front();
            ready.pop_front();
            h.resume();
        }
    }
};

协程在 await_suspend 时调用 Scheduler::schedule(h) 将自己重新加入队列,从而实现非阻塞事件驱动。

七、结语

C++20 的协程为异步编程提供了与同步代码同等的可读性与可维护性。通过 awaitable 对象包装标准库中的异步设施(如 std::futurestd::async),或者自定义事件驱动,开发者可以轻松实现高性能、低资源占用的 I/O 任务。随着社区生态的发展,越来越多的库(如 Boost.Asio、libuv 等)已经开始支持协程,使得 C++ 在高性能网络、游戏开发与系统编程领域拥有更广阔的前景。

发表评论