实现 C++20 中的协程(Coroutine)机制

C++20 标准正式引入协程(coroutine)特性,为异步编程和生成器提供了更为直观且高效的实现手段。协程本质上是一种能在执行过程中挂起(suspend)和恢复(resume)的函数,它们通过编译器生成的状态机来管理函数内部的局部变量与执行位置。本文将从概念入手,阐述协程的工作原理、关键语法,并给出一个简易的协程实现示例,帮助读者快速上手。

1. 协程的核心概念

术语 定义
co_await 用于挂起协程,等待异步操作完成后再恢复。
co_yield 用于生成器函数,返回一个值并挂起,随后可继续执行。
co_return 结束协程,返回最终结果。
promise_type 协程的承诺类型,负责协程状态的管理与结果返回。
awaitable 一个对象,定义了 await_readyawait_suspendawait_resume 三个成员,用来决定协程挂起与恢复的方式。

协程在编译时会被转换为一个类(或结构体),该类包含了函数体的所有局部变量和一个指向“下一个执行点”的状态机。挂起点会生成 promise_type 的实例,并在需要时返回给调用者。

2. 关键语法

// 声明协程返回类型
template<typename T>
struct task {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;
    handle_type coro_;
    explicit task(handle_type h) : coro_(h) {}
    ~task() { if (coro_) coro_.destroy(); }
    // 取值接口
    T get() { return coro_.promise().value; }
};

template<typename T>
struct task <T>::promise_type {
    T value;
    auto get_return_object() { return task{std::coroutine_handle <promise_type>::from_promise(*this)}; }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { std::terminate(); }
    template<typename U>
    void return_value(U&& v) { value = std::forward <U>(v); }
};

co_await 的使用

std::future <int> async_add(int a, int b) {
    co_return a + b;   // 简单的返回值
}

co_yield 的使用

task <int> generator() {
    for (int i = 0; i < 5; ++i) {
        co_yield i;   // 每次生成一个整数
    }
}

3. 简易协程实现示例

下面给出一个最小化的协程实现示例,实现一个异步计数器。我们使用 std::future 来模拟 awaitable 对象。

#include <coroutine>
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

// 简易 awaitable 对象
struct timer {
    std::chrono::milliseconds ms;
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, ms = ms]() {
            std::this_thread::sleep_for(ms);
            h.resume();
        }).detach();
    }
    void await_resume() const noexcept {}
};

// 计数器协程
task <int> async_counter(int start, int end, std::chrono::milliseconds step) {
    for (int i = start; i <= end; i += step.count()) {
        std::cout << "Count: " << i << std::endl;
        co_await timer{step};   // 挂起等待
    }
    co_return end;
}

int main() {
    auto t = async_counter(0, 10, std::chrono::milliseconds(500));
    std::cout << "Final value: " << t.get() << std::endl;
}

运行结果示例

Count: 0
Count: 5
Count: 10
Final value: 10

在上述代码中,timer 是一个自定义的 awaitable 对象,它在协程挂起时启动一个线程去延迟指定毫秒数,然后恢复协程。async_counter 协程在每次循环中打印计数并挂起,模拟了一个异步延迟计数器。最后通过 get() 方法获取协程返回的最终结果。

4. 协程与传统异步编程的对比

特点 传统回调 Promise/Future Coroutine
可读性 复杂嵌套 线性化但需要 then 代码几乎与同步代码相同
错误处理 回调链难以捕获异常 需要手动处理异常 直接使用 try/catch
性能 多层函数调用 轻量但仍有包装开销 状态机生成,无额外堆栈开销

协程在 C++20 以后已被广泛采用,它们通过编译器实现的状态机大幅提升了异步代码的可维护性与性能。

5. 进一步学习资源

  1. C++ Core Guidelines – Asynchronous Programming
  2. 《C++20标准》章节 30.11 协程
  3. Google’s gRPC C++ 协程示例
  4. Boost.Coroutine 2 (可兼容 C++14/17)

通过上述内容,你应该已经对 C++20 协程有了初步的理解。接下来可以尝试实现更复杂的协程,例如网络 I/O、任务调度器或并发生成器等。祝你编码愉快!

发表评论