C++20 中协程的实现与实践

C++20 协程是标准库的一大创新,为编写异步、延迟执行代码提供了新的语法糖。协程通过 co_yieldco_awaitco_return 三个关键字实现,背后使用的是一种名为 awaiter 的概念。本文将从协程的基本原理、关键特性以及常见使用场景三方面进行阐述,并给出一个简易的协程示例,帮助你快速上手。

1. 协程的基本概念

协程是一种可暂停和恢复的函数。与普通函数相比,协程可以在执行过程中挂起,并在稍后继续执行,而不是一次性完成所有计算。协程的核心是 挂起点(suspension point)。在 co_awaitco_yieldco_return 时,协程会进入挂起状态,并将控制权返回给调用者。随后,调用者可以决定何时恢复协程。

协程在 C++20 标准中通过 C++ coroutine library 实现。其实现主要涉及两个层面:

  1. 编译器层面:编译器将协程函数展开为一个状态机,将每个挂起点转换为一个 yield(或者 await)的状态点。
  2. 运行时层面:通过 std::experimental::coroutine_handlestd::coroutine_handle 对象管理协程的生命周期。

2. 核心关键字与语法

关键字 作用 例子
co_yield 产生一个值并挂起协程,类似于生成器 co_yield value;
co_await 等待一个 awaitable 对象完成,并挂起协程 auto result = co_await future;
co_return 结束协程并返回一个值 co_return final_value;

2.1 Awaitable 类型

要在协程中使用 co_await,必须提供一个 awaitable 对象。一个对象被视为 awaitable,需满足以下接口:

struct MyAwaitable {
    bool await_ready() noexcept; // 如果可以立即完成,返回 true
    void await_suspend(std::coroutine_handle<>) noexcept; // 挂起时调用
    auto await_resume() noexcept; // 恢复后返回结果
};

3. 协程的实现细节

协程的展开过程类似于编译器将函数拆分成若干段,每段之间由 if 语句连接,形成一个状态机。具体步骤如下:

  1. 生成状态机类:编译器会生成一个内部类,用来保存协程状态(局部变量、返回点等)。
  2. 生成 promise_type:每个协程都需要一个 promise_type,负责管理协程的生命周期,返回值,以及错误处理。
  3. 生成 coroutine_handle:通过 `coroutine_handle ` 对象,调用者可以获取协程的句柄,用于恢复、销毁协程。

4. 常见使用场景

  1. 异步 I/O:协程与 std::asyncstd::future 结合,能够写出像同步代码一样的异步逻辑。
  2. 生成器:利用 co_yield,实现惰性序列生成,类似 Python 的生成器。
  3. 任务调度:协程可以与事件循环结合,用于实现协作式多任务。

5. 简易协程示例

下面给出一个使用协程实现整数序列生成器的完整示例。

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

// 生成器的 Promise 类型
struct IntGenerator {
    struct promise_type {
        int current_value = 0;

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

        // co_yield 调用时触发
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> handle;

    // 迭代器
    struct iterator {
        std::coroutine_handle <promise_type> h;

        iterator(std::coroutine_handle <promise_type> h) : h(h) {}
        iterator& operator++() {
            h.resume();
            return *this;
        }
        bool operator!=(const iterator& other) const { return h != other.h; }
        int operator*() const { return h.promise().current_value; }
    };

    iterator begin() { return iterator(handle); }
    iterator end() { return iterator(nullptr); }
};

IntGenerator count_up_to(int n) {
    for (int i = 0; i <= n; ++i)
        co_yield i;
}

int main() {
    for (int x : count_up_to(5))
        std::cout << x << ' ';   // 输出: 0 1 2 3 4 5
}

代码说明

  • IntGenerator 定义了一个协程生成器,并提供了 begin/end 迭代器接口。
  • promise_type 中的 yield_valueco_yield 触发时被调用,保存当前值。
  • count_up_to 函数通过 co_yield 生成 0~n 的整数序列。

6. 性能与注意事项

  • 栈占用:协程本身不需要额外栈空间,只有局部变量会被保存在 promise_type 中。
  • 异常传播promise_type::unhandled_exception 负责捕获协程内部抛出的异常。
  • 多线程:协程本身是线程安全的,但 coroutine_handle 的恢复与销毁需在同一线程或使用同步机制。

7. 结语

C++20 协程为语言增添了强大的异步编程能力。虽然刚开始学习时可能会感觉概念繁琐,但随着实际项目中的应用,你会发现协程极大地简化了异步代码的结构。建议从生成器、异步 I/O 等简单场景入手,逐步扩展到更复杂的协程框架。祝你在 C++ 编程的道路上玩得开心!

发表评论