如何在C++中实现协程(Coroutine)?

协程是现代C++(尤其是C++20及以后)中非常重要的特性,能够让函数挂起和恢复,从而实现更高效的异步编程、生成器以及状态机等功能。下面我们从基本概念到实现细节,全面剖析C++协程的使用方法。

一、协程基础

  1. 挂起(suspend)与恢复(resume)
    协程可以在执行过程中被挂起(suspend),在某个条件满足后再恢复执行(resume)。挂起时协程的局部状态会被保存,以便恢复时继续执行。

  2. 生成器(generator)
    一种典型的协程应用,即通过co_yield一次产生一个值,调用方使用co_await或循环来获取下一个值。

  3. 任务(task)
    通过co_return返回最终结果,或者在协程内部使用co_return或直接返回一个值。

二、C++20 协程关键字

关键字 用途
co_await 等待异步操作完成,挂起协程
co_yield 生成值,挂起并返回值给调用方
co_return 结束协程,返回最终值
co_return; 仅结束协程,不返回值

三、核心概念:promise 与 handle

C++协程的实现基于两个核心概念:

  1. promise
    协程中 co_awaitco_yieldco_return 等操作都会与 promise 交互。promise 定义了协程的行为,例如何时挂起、恢复、返回值、异常处理等。

  2. handle
    `std::coroutine_handle

    ` 用来管理协程的生命周期。可以使用 `handle.resume()` 恢复协程,`handle.done()` 判断是否结束。

四、实现一个简单生成器

下面给出一个完整例子,实现一个产生整数序列的协程生成器。

#include <coroutine>
#include <iostream>
#include <vector>

// 生成器的 promise 类型
template<typename T>
struct Generator {
    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() {
            return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void unhandled_exception() { std::exit(1); }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> handle;

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

    bool move_next() {
        if (!handle.done()) {
            handle.resume();
        }
        return !handle.done();
    }

    T current() const { return handle.promise().current_value; }
};

Generator <int> range(int start, int end) {
    for (int i = start; i <= end; ++i) {
        co_yield i;
    }
}

int main() {
    for (auto g = range(1, 5); g.move_next(); ) {
        std::cout << g.current() << " ";
    }
    // 输出: 1 2 3 4 5
}

代码说明

  • promise_type 定义了 yield_value 让协程挂起并保存当前值。
  • initial_suspendfinal_suspend 分别决定协程是否立即挂起以及结束时的挂起行为。
  • Generator 包装了 std::coroutine_handle,提供 move_next()current() 两个接口,使用者可以像普通迭代器一样使用。
  • range 函数示例演示了如何使用 co_yield 产生一系列整数。

五、协程与异步 I/O

C++协程最常见的场景是与异步 I/O 结合,例如 boost::asio 或自定义 async_wait。示例:

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

struct Timer {
    struct promise_type;
    using handle_t = std::coroutine_handle <promise_type>;

    struct promise_type {
        std::chrono::steady_clock::time_point when;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Timer get_return_object() { return Timer{handle_t::from_promise(*this)}; }
        void unhandled_exception() { std::exit(1); }
        void return_void() {}
    };

    handle_t handle;
    Timer(handle_t h) : handle(h) {}
    ~Timer() { if (handle) handle.destroy(); }

    void start() {
        std::thread([h = handle] {
            std::this_thread::sleep_until(h.promise().when);
            h.resume(); // 完成后恢复协程
        }).detach();
    }
};

async Task
async_sleep(std::chrono::milliseconds ms) {
    Timer timer;
    timer.handle.promise().when = std::chrono::steady_clock::now() + ms;
    timer.start();
    co_await std::suspend_always{}; // 等待 timer 触发
}

注意:标准库中并没有直接提供 std::suspend_always 等待外部事件的实现,通常需要借助第三方库或平台特定的事件循环。

六、错误处理

协程中出现异常时,promise_type::unhandled_exception() 会被调用。可以在此处捕获异常并做日志或状态转移。例如:

void unhandled_exception() {
    std::exception_ptr eptr = std::current_exception();
    try { std::rethrow_exception(eptr); }
    catch (const std::exception &e) {
        std::cerr << "协程异常: " << e.what() << '\n';
    }
}

七、性能与注意事项

  • 栈分配:协程本质上是生成一个堆分配的状态机,对每个协程实例会产生额外内存开销。
  • 生命周期:需要注意 handle 的生命周期,避免悬挂引用。
  • 异常安全:确保在 promise_type::unhandled_exception() 中妥善处理异常,防止资源泄露。

八、总结

C++20 协程提供了强大的语法糖,使得异步编程和生成器等复杂逻辑变得更加简洁。通过理解 promisehandleco_await/co_yield/co_return 的交互,你可以在自己的项目中灵活使用协程,提升代码可读性和性能。祝你玩得开心,编码愉快!

发表评论