**如何在C++中实现一个简易的协程库?**

在 C++20 标准中引入了协程(coroutine)这一强大功能,但对于许多开发者而言,直接使用 std::generator 或第三方库(如 Boost.Coroutine)仍然显得繁琐。本文将演示如何用最小的代码量实现一个简易的协程框架,帮助你快速理解协程的工作机制,并在自己的项目中快速实验。


1. 先说说协程的核心概念

协程是一种可暂停、可恢复的函数,内部的状态会在暂停时被保存,下次继续执行时从上次中断的位置恢复。C++20 对协程的支持核心是:

  • co_awaitco_yieldco_return 三个关键字。
  • 悬挂点(promise):协程体内部的执行与外部协作的桥梁。
  • std::coroutine_handle:可用来手动管理协程的生命周期。

我们会用到 std::promise 结构来实现协程的返回值和状态。


2. 一个最小化的协程包装器

#include <coroutine>
#include <iostream>
#include <exception>

template<typename T>
class Coroutine {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        auto get_return_object() {
            return Coroutine{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exception_ = std::current_exception(); }

        template<typename U>
        auto yield_value(U&& v) {
            value_ = std::forward <U>(v);
            return std::suspend_always{};
        }

        void return_value(T v) { value_ = std::move(v); }
    };

    Coroutine(handle_type h) : h_(h) {}
    ~Coroutine() { if (h_) h_.destroy(); }

    bool resume() {
        if (!h_.done()) {
            h_.resume();
            return !h_.done();
        }
        return false;
    }

    T get() {
        if (h_.promise().exception_)
            std::rethrow_exception(h_.promise().exception_);
        return h_.promise().value_;
    }

private:
    handle_type h_;
};

说明

  • promise_type:保存协程的返回值和异常。
  • initial_suspendfinal_suspend:均返回 suspend_always,保证协程在 co_await 前先暂停,结束时也会暂停,方便外部手动调用 resume()
  • yield_value:把协程产出的值写进 value_,随后暂停。
  • return_value:协程结束时写入最终结果。

3. 一个简单的协程使用示例

// 生成斐波那契数列的协程
Coroutine <int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int tmp = a;
        a = b;
        b = tmp + b;
    }
    co_return a;  // 最后一个数
}

int main() {
    auto fib = fibonacci(10);
    while (fib.resume()) {
        std::cout << fib.get() << ' ';
    }
    std::cout << "final: " << fib.get() << '\n';
    return 0;
}

输出

0 1 1 2 3 5 8 13 21 34 final: 55

这里的 fib.get()resume() 调用后会拿到刚刚被 co_yield 暂停的值。final_suspend() 后的 resume() 会使协程结束,此时 get() 返回 co_return 的值。


4. 与标准库协程的整合

C++20 只提供了底层 API,若想更方便地使用,可通过包装 std::generator(实验性)或 std::experimental::generator 实现。下面给出一个更通用的生成器实现:

template<typename T>
class Generator {
public:
    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        auto get_return_object() {
            return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exception_ = std::current_exception(); }

        template<typename U>
        auto yield_value(U&& v) {
            value_ = std::forward <U>(v);
            return std::suspend_always{};
        }

        void return_void() {}
    };

    using handle_type = std::coroutine_handle <promise_type>;

    Generator(handle_type h) : h_(h) {}
    ~Generator() { if (h_) h_.destroy(); }

    bool next() {
        if (!h_.done()) {
            h_.resume();
            return !h_.done();
        }
        return false;
    }

    T current() {
        if (h_.promise().exception_)
            std::rethrow_exception(h_.promise().exception_);
        return h_.promise().value_;
    }

private:
    handle_type h_;
};

使用方式与前面相似,区别在于:

  • return_void():协程不需要返回值,只负责产出。
  • next():继续到下一个 yield
  • current():读取最新的产出值。

5. 常见坑和调试技巧

  1. 忘记 resume()
    协程默认在创建时暂停,需要手动调用 resume() 才能开始执行。常见错误是直接调用 get(),会得到未初始化的值。

  2. 异常泄漏
    若协程内部抛异常,需要在 promise_type::unhandled_exception 捕获。否则外部无法获取异常信息。

  3. 多次 resume() 后协程已结束
    resume() 在协程结束后会返回 false,此时再调用 resume() 仍会返回 false。可通过 handle.done() 检查。

  4. 资源泄漏
    handle.destroy() 必须在协程完成后调用。我们在 Coroutine/Generator 的析构中完成。


6. 小结

  • C++20 的协程是通过 promise_typecoroutine_handle 机制实现的。
  • 本文提供了一个最小化的协程框架,支持 co_yieldco_return
  • 示例展示了斐波那契数列生成器,演示了协程的暂停与恢复。
  • 对比标准库实验性 generator,可根据项目需求自行选择实现方式。

使用协程能够让你以更直观的方式处理异步/生成器逻辑,建议在高并发或事件驱动场景下尝试。祝编码愉快!

发表评论