C++20 协程的使用与实现原理

在 C++20 标准中,协程(coroutine)被正式纳入语言核心,为异步编程和生成器提供了更简洁、更高效的语法。本文将从协程的基本概念、关键字、状态机实现以及实际应用场景四个方面,详细剖析 C++20 协程的使用与实现原理。

1. 协程的基本概念

协程是一种轻量级的用户级线程,它允许函数在执行过程中暂停并在之后恢复。与传统线程相比,协程不需要操作系统调度,切换成本极低。协程主要由三部分组成:

  1. 协程函数:使用 co_await, co_yieldco_return 的函数体。
  2. 协程句柄(promise):协程的状态管理对象,负责协程生命周期、异常处理以及返回值。
  3. 协程状态机:编译器根据协程函数生成的状态机实现,维护协程的执行状态。

2. 关键字与基本语法

2.1 co_await

co_await 用于等待一个 awaitable 对象,类似于 async/await 的 await。它会将协程挂起,直到 awaitable 对象完成。

co_await asyncTask();

2.2 co_yield

co_yield 用于生成值,典型用法是实现生成器:

generator <int> getNumbers() {
    for (int i = 0; i < 10; ++i)
        co_yield i;
}

2.3 co_return

co_return 用于返回协程的最终结果,结束协程执行。

int result = co_return compute();

3. 协程句柄与 Promise 对象

编译器会根据协程函数生成两个类:

  • promise_type:用户自定义,存放协程内部状态、返回值等。
  • **coroutine_handle **:句柄,指向 promise,提供 `resume()`、`destroy()` 等操作。

3.1 promise_type 必需成员

成员 说明
auto get_return_object() 返回协程句柄或包装类型
std::suspend_always initial_suspend() 初始挂起行为
std::suspend_always final_suspend() 最终挂起行为
void return_value(T value) 处理 co_return 的值
void unhandled_exception() 异常处理

3.2 典型实现:生成器

template<typename T>
class generator {
public:
    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 return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    using handle_type = std::coroutine_handle <promise_type>;

    generator(handle_type h) : handle(h) {}
    ~generator() { if (handle) handle.destroy(); }

    bool next() { return handle.resume(); }
    T value() const { return handle.promise().current_value; }

private:
    handle_type handle;
};

4. 协程状态机的实现原理

当编译器遇到协程函数时,实际上它会生成一个状态机:

  1. 状态变量:用于标记协程的执行点,类似于 switch-case。
  2. 堆栈/寄存器保存:在挂起时,将必要的局部变量存储在堆上。
  3. 调用链resume() 将根据当前状态跳转到对应代码块。

状态机的核心是 co_awaitco_yield 触发的挂起点。每个挂起点都有一个对应的“resume point”,在 resume() 时,编译器会恢复到上一次挂起的地方继续执行。

4.1 代码示例

generator <int> foo() {
    co_yield 1;  // 第一次挂起
    co_yield 2;  // 第二次挂起
    co_yield 3;  // 第三次挂起
}

编译后大致会变成:

bool foo_body(generator <int>* self) {
    switch (self->state) {
        case 0: self->state = 1; self->value = 1; return true;
        case 1: self->state = 2; self->value = 2; return true;
        case 2: self->state = 3; self->value = 3; return true;
        default: return false;
    }
}

每一次 next() 调用会更新 state 并返回相应的值。

5. 实际应用场景

  1. 异步 I/O
    协程可以与事件循环(如 libuv、asio)配合,实现非阻塞网络通信。

    async_socket socket;
    auto data = co_await socket.read_some(buffer);
  2. 生成器
    如上所示,用 co_yield 实现惰性序列,适用于大数据流、文件逐行读取等。

  3. 协程池
    将多个协程包装为任务,使用线程池调度,避免线程上下文切换。

  4. 游戏脚本
    在游戏引擎中,协程可以实现角色行为、动画脚本等。

6. 性能对比

与传统线程相比:

  • 创建成本:协程仅需栈帧一次,几乎无额外开销。
  • 切换成本:协程切换仅涉及局部变量的保存与恢复,远低于线程上下文切换。
  • 内存占用:协程状态机通常几百字节,远小于线程堆栈(几百 KB)。

7. 常见坑与注意事项

  1. 未定义行为:在 co_yield 之后立即访问未初始化的局部变量会导致 UB。
  2. 异常传播promise_type::unhandled_exception 必须处理,否则协程崩溃。
  3. 生命周期管理:句柄销毁需谨慎,避免悬挂。
  4. 协程对象拷贝:默认不可拷贝,若需要拷贝需自行实现。

8. 结语

C++20 协程为 C++ 提供了一套优雅的异步编程模型。通过深入理解其关键字、句柄与状态机实现,开发者可以在保持代码可读性的同时,写出高效、可维护的异步代码。随着标准库及第三方库的完善,协程将在更广泛的领域得到应用,从网络编程到游戏开发,再到大数据处理,协程都将成为不可或缺的工具。

发表评论