C++ 23 中的协程:从设计到实践

协程(Coroutine)在 C++ 23 中成为标准的一部分,它为编写异步、惰性计算以及可组合的控制流提供了更简洁、更高效的手段。本文将从协程的基本概念出发,逐步介绍其语法、关键类型、实现细节,并给出一个完整的实战示例,帮助读者快速掌握协程的使用方法。

1. 协程的基本概念

协程是一种“轻量级线程”,可以在执行期间暂停(yield)并在需要时恢复。与传统的 std::asyncstd::thread 不同,协程的切换是由编译器生成的,开销极低。协程通过 co_await, co_yieldco_return 三个关键字实现协作式暂停与恢复。

  • co_await:等待一个 awaitable 对象完成,若对象未就绪则挂起协程。
  • co_yield:产生一个值并挂起协程,等待下次继续。
  • co_return:返回一个最终值,结束协程。

协程本质上是一个返回类型为 std::coroutine_handle<> 的函数,编译器会在内部生成一个状态机。

2. 关键类型与约束

  • std::suspend_always / std::suspend_never:可用于控制协程挂起行为。
  • std::promise_type:每个协程生成器都有一个关联的 promise 类型,用来存储结果、异常等。
  • `std::coroutine_handle `:表示对协程的句柄,可用于启动、检查状态和恢复。

协程函数的签名通常是:

auto generator() -> std::generator <int>; // 需要 C++23

或者手动实现:

struct MyPromise {
    int current;
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(int val) { current = val; return {}; }
    void return_void() {}
};

auto generator() -> std::generator <int> { ... } // 依赖标准库实现

3. 协程与传统异步的区别

特点 传统异步(如 std::future) 协程
调度 由线程池或事件循环手动管理 由编译器生成的状态机自动管理
开销 线程切换、锁 非抢占式切换,几乎无上下文切换
代码可读性 回调地狱 线性可读的同步式写法

4. 一个完整的协程示例

下面演示一个使用 std::generator 的协程实现,生成斐波那契数列,并在主线程中异步消费。

#include <iostream>
#include <generator>
#include <chrono>
#include <thread>

// 生成斐波那契数列的协程
std::generator<long long> fib(long long n) {
    long long a = 0, b = 1;
    for (long long i = 0; i < n; ++i) {
        co_yield a;          // 产生当前值
        std::swap(a, b);
        b += a;
        // 模拟耗时操作
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::cout << "Fibonacci sequence (first 15 numbers):\n";
    auto g = fib(15);            // 创建协程生成器
    for (auto val : g) {         // 迭代消费
        std::cout << val << ' ';
    }
    std::cout << '\n';
    return 0;
}

说明

  • std::generator<long long> 是标准库提供的协程包装,内部已经实现了必要的 promise 类型。
  • co_yield 将当前值推送给消费者,同时挂起协程。
  • for (auto val : g) 采用范围基循环消费协程,编译器会生成相应的 operator++operator*

运行结果示例:

Fibonacci sequence (first 15 numbers):
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 

5. 高级技巧

5.1 自定义 Awaitable

可以将自定义类包装为 awaitable,实现异步等待。示例:异步文件读取。

struct AsyncRead {
    int fd;
    std::size_t size;
    char* buffer;
    std::suspend_always await_ready() const noexcept { return {}; }
    std::suspend_always await_suspend(std::coroutine_handle<> h) const noexcept {
        // 在 I/O 线程中启动读操作,完成后恢复 h
        std::thread([=, h]() {
            // 模拟读操作
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
            // ...实际读写到 buffer
            h.resume();
        }).detach();
        return {};
    }
    std::size_t await_resume() const noexcept { return size; }
};

5.2 组合协程

协程之间可以相互 co_await,形成嵌套或流水线。

std::generator <int> even_numbers() {
    for (int i = 0; i < 10; i += 2) co_yield i;
}

std::generator <int> odd_numbers() {
    for (int i = 1; i < 10; i += 2) co_yield i;
}

std::generator <int> all_numbers() {
    for (auto e : even_numbers()) co_yield e;
    for (auto o : odd_numbers()) co_yield o;
}

6. 性能与注意事项

  • 内存占用:协程生成器在栈上分配状态机,避免堆分配,开销低。
  • 异常传播:异常会在协程中被捕获并存储在 promise_type::unhandled_exception(),可通过 await_resume() 重新抛出。
  • 生命周期:协程句柄生命周期必须与协程生成器匹配,避免悬空句柄。

7. 小结

C++ 23 中的协程为编写高效、可组合的异步代码提供了强大工具。通过 std::generator 等标准包装,开发者可以用几行代码完成复杂的流式处理。掌握 co_awaitco_yieldco_return 的使用,理解协程状态机的实现细节,将使你在现代 C++ 开发中更加游刃有余。祝你编码愉快!

发表评论