C++ 20 的协程(coroutine)实战:从基础到高级

协程(coroutine)是 C++ 20 标准中新增的语言特性,旨在让异步编程和生成器的实现变得更简洁、可读。相比传统的基于回调或线程的异步方案,协程在性能、可维护性和错误处理上都有显著优势。本文将从协程的基本概念出发,逐步介绍其实现细节、常用标准库支持,以及在实际项目中的应用示例。

1. 协程的基本概念

协程是一种“可挂起”的函数。它的执行可以在任意点暂停(co_awaitco_yieldco_return),然后在后续恢复。协程的本质是一个状态机,编译器会把协程的代码自动拆分成若干个状态并生成对应的控制结构。

  • co_await:挂起协程并等待某个可等待对象(Awaitable)的完成,随后恢复执行。
  • co_yield:在生成器协程中返回一个值,并挂起协程,等待下一次迭代。
  • co_return:结束协程,返回最终结果(仅在返回值的协程中使用)。

协程可以像普通函数那样被调用,但它们并不立即执行全部代码,而是返回一个 任务std::futurestd::generator 等),此任务代表协程的生命周期。

2. 标准库中的协程支持

C++ 20 标准库为协程提供了几个重要的类型,最常用的包括:

类型 作用 示例
`std::future
| 代表一个异步计算,最终会得到一个值T|std::future fut = async_func();`
`std::generator
| 生成器类型,用co_yield生成序列 |std::generator gen = sequence();`
`std::task
| 自定义的协程返回值类型,类似于future|std::task async_main();`
std::suspend_always / std::suspend_never 简单的挂起策略,用于控制协程的挂起与否 co_await std::suspend_always{};

2.1 std::futurestd::async

std::async 可以直接用来创建协程函数,但在标准中没有将其标记为协程。若要真正利用协程语法,需要自定义返回类型,例如:

struct async_int {
    struct promise_type {
        int value_;
        async_int get_return_object() { return async_int{std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(int v) noexcept { value_ = v; }
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle <promise_type> coro_;
    int get() { return coro_.promise().value_; }
};

然后使用:

async_int fetch_number() {
    co_return 42;
}

3. 生成器(generator)的实战

生成器是协程最典型的用例。使用 std::generator 可以轻松实现惰性序列,例如斐波那契数列:

std::generator <int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int temp = a + b;
        a = b;
        b = temp;
    }
}

使用方式:

for (int x : fibonacci(10)) {
    std::cout << x << ' ';
}

这段代码会按需生成斐波那契数值,而不会一次性占用大量内存。

4. 异步 I/O 的协程化

在网络编程或文件 I/O 场景,协程可配合 asiolibuv 等库进行异步 I/O。以 asio 为例:

#include <asio.hpp>
using asio::ip::tcp;

asio::awaitable <void> async_echo(tcp::socket sock) {
    char data[1024];
    std::size_t n = co_await sock.async_read_some(asio::buffer(data), asio::use_awaitable);
    co_await sock.async_write_some(asio::buffer(data, n), asio::use_awaitable);
}

这里 asio::awaitable 内部封装了 asio::coroutine_handle,让 asio 的异步调用变得像同步代码一样可读。

5. 协程调度器(Scheduler)的实现

标准库并未提供完整的协程调度器,仅提供了基本的挂起/恢复机制。若需要实现自定义调度器(如优先级队列、时间片轮转等),可使用 std::coroutine_handlestd::suspend_always 组合:

struct scheduler {
    std::vector<std::coroutine_handle<>> tasks_;

    void push(std::coroutine_handle<> h) { tasks_.push_back(h); }

    void run() {
        while (!tasks_.empty()) {
            auto h = tasks_.back();
            tasks_.pop_back();
            h.resume();
        }
    }
};

在协程函数里,通过 co_await 某个自定义 Awaitable 将协程挂起,并在调度器中重入。

6. 常见坑与建议

  1. 生命周期管理
    协程内部返回的对象若持有协程句柄,必须确保协程未被销毁。使用 std::shared_future 或自定义计数器可以帮助管理。

  2. 异常传播
    co_await 的 Awaitable 必须实现 await_resume,若异常发生应通过 unhandled_exceptionreturn_void 传递。避免在协程内部忽略异常导致程序崩溃。

  3. 性能考量
    虽然协程在逻辑上比回调更直观,但如果过度使用 co_yield 或频繁挂起,可能导致堆栈分配频繁。必要时可结合 std::pmr 或自定义内存池。

7. 结语

C++ 20 的协程特性为异步编程提供了新的表达方式。掌握协程的基本概念、标准库类型以及与常见 I/O 库的配合使用,可以让代码更简洁、易维护,并在性能上取得显著提升。建议从简单的生成器开始练手,逐步过渡到网络 I/O 与自定义调度器,深入了解协程的底层实现细节,为将来的大型项目奠定坚实基础。

发表评论