**什么是C++20协程?如何在C++中实现异步I/O?**

C++20 引入了协程(coroutine)概念,它让异步编程变得更直观、易维护。协程本质上是一种可以“挂起”和“恢复”的函数,编译器负责将普通函数拆解成若干状态机片段,调度器则在需要时恢复它们。下面我们从基本概念到实际异步 I/O 示例,逐步剖析协程的实现方式。


1. 协程的核心概念

1.1 协程的结构

  • 挂起点 (co_await, co_yield, co_return)
    协程在遇到这些关键字时会暂停执行,保存其内部状态,返回给调用者。
  • 状态机
    编译器把协程函数编译成一个类,内部拥有 promise_typehandle、状态机逻辑。
  • 协程句柄 (std::coroutine_handle)
    用于手动管理协程的生命周期,调用 resume() 恢复执行,destroy() 结束。

1.2 promise_type

每个协程都有一个 promise_type,它定义了协程执行时的行为,例如:

struct MyPromise {
    MyPromise() = default;
    std::suspend_always initial_suspend() noexcept { return {}; } // 第一次挂起
    std::suspend_always final_suspend() noexcept { return {}; }  // 最后挂起
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
};

initial_suspendfinal_suspend 可以返回 suspend_neversuspend_always,控制协程启动与结束时是否立即挂起。


2. 简单协程示例

2.1 计数协程

#include <coroutine>
#include <iostream>

struct Counter {
    struct promise_type {
        Counter get_return_object() { return {std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> handle;
    explicit Counter(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~Counter() { if (handle) handle.destroy(); }
};

Counter count_to(int n) {
    for (int i = 1; i <= n; ++i) {
        std::cout << i << '\n';
        co_await std::suspend_always{}; // 每次打印后挂起
    }
}

调用:

auto coro = count_to(5);
coro.handle.resume(); // 1
coro.handle.resume(); // 2
// ...

3. 协程与异步 I/O

3.1 传统异步 I/O

在经典 C++ 中,异步 I/O 通常通过回调、std::future/std::promise 或第三方库(如 Boost.Asio)实现。代码往往堆叠回调,导致“回调地狱”。

3.2 协程实现异步 I/O

使用 co_await 可以把异步操作当作同步语句书写,读起来更像线性流程。下面演示基于 Boost.Asio 的异步文件读取,改写为协程:

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::awaitable;
using asio::use_awaitable;
using asio::ip::tcp;

awaitable<std::size_t> async_read_file(const std::string& path, std::vector<char>& buffer) {
    asio::io_context& ioc = co_await asio::this_coro::executor;
    asio::posix::stream_descriptor fd(ioc, ::open(path.c_str(), O_RDONLY));

    std::size_t total = 0;
    while (true) {
        std::size_t n = co_await fd.async_read_some(
            asio::buffer(buffer.data() + total, buffer.size() - total), use_awaitable);
        if (n == 0) break;           // EOF
        total += n;
    }
    fd.close();
    co_return total;                  // 返回读取字节数
}

主程序

int main() {
    asio::io_context ioc;
    std::vector <char> buf(1024);
    auto fut = async_read_file("example.txt", buf);
    std::size_t bytes = fut.get();   // 这里会阻塞直到协程完成
    std::cout << "Read " << bytes << " bytes.\n";
    return 0;
}

关键点

  • co_awaitasync_read_file 中挂起,等待 I/O 完成后恢复。
  • `awaitable ` 是 Boost.Asio 的协程包装器,内部包含 `promise_type`。
  • 这样写法与同步 I/O 结构极为相似,避免了回调链。

4. 自定义 awaitable

如果你不想依赖第三方库,也可以手动实现一个 awaitable

template<typename T>
struct SimpleAwaitable {
    T value_;
    bool ready_ = false;
    std::function<void()> resume_cb_;

    SimpleAwaitable(T val) : value_(val) {}

    struct awaiter {
        SimpleAwaitable <T>& awaitable_;
        bool await_ready() { return awaitable_.ready_; }
        void await_suspend(std::coroutine_handle<> h) {
            awaitable_.resume_cb_ = [h](){ h.resume(); };
        }
        T await_resume() { return awaitable_.value_; }
    };

    awaiter operator co_await() { return { *this }; }
};

SimpleAwaitable <int> async_compute() {
    // 模拟异步计算
    int result = 42;
    co_return result;
}

5. 结合 std::future 与协程

如果你想把传统 std::future 与协程结合,C++23 提供了 std::futureco_await 支持。示例:

std::future <int> async_square(int x) {
    return std::async(std::launch::async, [x] { return x * x; });
}

awaitable <int> wrapper() {
    int value = co_await async_square(5); // 自动等待 future 完成
    co_return value * 2;
}

6. 性能与安全

  • 堆栈:协程的挂起点只保存局部状态,实际堆栈不被压入,开销低。
  • 异常:通过 promise_type::unhandled_exception() 捕获异常,避免崩溃。
  • 资源管理:协程句柄必须在结束时 destroy(),否则会泄漏。
  • 调试:在调试时使用 -fcoroutines 或对应编译器标记,查看生成的状态机代码。

7. 结语

C++20 协程为异步编程提供了“同步化”语法糖,使得代码更加可读、易维护。通过协程,你可以像写同步代码那样写异步 I/O、网络通信、任务调度等。虽然编译器会生成复杂的状态机,但对程序员而言,协程隐藏了这一层细节,让你专注于业务逻辑。未来随着标准库持续完善,协程将成为 C++ 编程不可或缺的一部分。

发表评论