C++ 中的协程:从概念到实践

协程(coroutine)是 C++20 标准中引入的一项强大功能,它让我们可以在单线程环境下实现类似多线程的并发执行。与传统的线程相比,协程的上下文切换开销极低,能够在不产生额外线程的情况下实现异步编程。下面我们从协程的基本概念、实现原理、常见使用场景以及实际编码示例几个方面来探讨协程在 C++ 开发中的应用。

一、协程的基本概念

  1. 协作式多任务:协程是一种“合作式”的并发模型。执行流程需要协程自身主动挂起(co_awaitco_yieldco_return),而不是被操作系统强制抢占。
  2. 协程句柄(coroutine handle)std::coroutine_handle 对象封装了协程的执行上下文。通过它可以手动启动、暂停或销毁协程。
  3. 悬挂点(suspension point):在协程函数体内,遇到 co_awaitco_yieldco_return 时会产生悬挂点,协程的状态会被保存,并返回给调用者。
  4. 尾随返回类型(co_return):协程函数的返回类型需要实现 promise_type,它定义了协程完成时的行为,例如如何返回值、如何处理异常等。

二、协程的实现原理

  • 状态机化:编译器把协程函数编译成一个状态机,状态机的每个分支对应一个悬挂点。
  • 栈上内存:协程的局部变量(在 co_yield/co_await 前后需要保留的)被拆解成“悬挂点状态”,存放在堆或栈上。
  • promise 对象:每个协程都有一个 promise_type 对象,负责管理协程的生命周期、返回值、异常等。
  • 协程句柄:通过 `std::coroutine_handle ` 与协程进行交互,启动或继续协程执行。

三、典型使用场景

  1. 异步 I/O:结合 co_await 与异步 I/O 库(如 asio)实现无阻塞网络通信。
  2. 协程生成器:利用 co_yield 创建惰性序列,例如无限斐波那契数列。
  3. 任务调度:将协程与事件循环结合,构建轻量级调度器,实现高性能并发服务器。
  4. 状态机实现:将传统的 if-else 或 switch 逻辑转换为协程式状态机,代码更易维护。

四、代码示例

下面给出一个简单的协程生成器示例,演示如何使用 co_yield 产生无限斐波那契数列,并在主函数中迭代读取前 10 个值。

#include <iostream>
#include <coroutine>
#include <optional>

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() {}
    };

    using handle_type = std::coroutine_handle <promise_type>;

    handle_type coro;

    explicit generator(handle_type h) : coro(h) {}
    generator(const generator&) = delete;
    generator(generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
    ~generator() { if (coro) coro.destroy(); }

    bool next() { return coro.resume(); }

    T value() const { return coro.promise().current_value; }
};

generator<unsigned long long> fib_sequence(unsigned long long limit) {
    unsigned long long a = 0, b = 1;
    while (a <= limit) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    auto seq = fib_sequence(100);
    int count = 0;
    while (seq.next() && count < 10) {
        std::cout << seq.value() << ' ';
        ++count;
    }
    std::cout << '\n';
    return 0;
}

运行结果

0 1 1 2 3 5 8 13 21 34 

五、常见坑与注意事项

  1. 悬挂点的生命周期:确保协程句柄在协程完成前未被销毁,否则会导致未定义行为。
  2. 异常安全:如果协程中抛出异常,promise_type::unhandled_exception 会被调用。可以在这里做清理或记录日志。
  3. 协程句柄的拷贝:默认不可拷贝,建议使用移动语义。
  4. 协程与线程交互:在多线程环境中使用协程时,需要注意线程安全,例如对 promise_type 的访问进行同步。

六、总结

C++20 的协程为实现高性能、低开销的异步编程提供了天然的工具。掌握协程的基本概念、编译实现以及常见使用场景,能够让我们在构建网络服务器、游戏引擎、数据处理管道等领域时写出更简洁、更易维护的代码。随着 C++ 标准库与生态的不断完善,协程将成为现代 C++ 开发不可或缺的一部分。

发表评论