C++20 的协程(Coroutines)到底是怎么工作的?

C++20 引入了协程(Coroutines)这一强大的语言特性,使得异步编程、生成器以及复杂的状态机逻辑可以用更直观、更简洁的语法来实现。下面我们从理论、实现细节、使用场景以及常见坑四个方面,系统性地解析协程的工作原理,并给出示例代码,帮助你快速上手。


1. 何为协程?

协程是一种“可挂起”的函数。与普通函数的执行流程不同,协程可以在任意位置暂停(co_awaitco_yieldco_return),随后恢复执行,且恢复时会记住之前的局部状态。这样就能把一个“连续的执行流”拆分成若干“断点”,每一次调用都能让协程从上一次停下的地方继续运行。

C++20 对协程的语法支持包括:

  • co_await:挂起协程,等待一个 awaitable 对象完成。
  • co_yield:生成一个值并挂起协程,常用于实现生成器。
  • co_return:返回协程结果,结束协程。

2. 协程的底层结构

C++ 协程实际上是编译器把一个普通函数拆分成若干 resume points(恢复点)并生成一个“状态机”对象。下面给出简化的步骤:

  1. 编译阶段

    • 编译器将 co_awaitco_yieldco_return 所在的位置记录为 yield points
    • 对函数的每个 co_yield 生成对应的 resume 函数。
    • 生成一个隐藏的 promise 对象,用来维护协程的状态(比如返回值、异常、挂起点)。
  2. 运行时阶段

    • 调用协程时,编译器会创建一个 `std::coroutine_handle ` 对象,并把控制权交给协程。
    • 当协程遇到 co_awaitco_yieldco_return 时,状态机会把当前状态存储在 promise 对象中,并把控制权返回给调用者。
    • 调用者可以再次 resume 协程,恢复到最近一次挂起的位置,继续执行。

重要概念

名称 作用
promise_type 协程的“上下文”,负责管理返回值、异常和挂起点。
coroutine_handle 用来手动控制协程的句柄,支持 resume()destroy()done() 等方法。
awaitable 一个可被 co_await 的对象,需要实现 await_ready()await_suspend()await_resume()

3. 示例:实现一个简单的生成器

下面的代码演示如何实现一个产生整数序列的协程生成器。

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

template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::suspend_always yield_value(const 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(); }
    };

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

    struct iterator {
        std::coroutine_handle <promise_type> coro;
        bool operator!=(const iterator& other) const { return coro != other.coro; }
        iterator& operator++() {
            coro.resume();
            if (!coro.done() && !coro.promise().current_value) {
                // skip empty values if needed
            }
            return *this;
        }
        const T& operator*() const { return coro.promise().current_value; }
    };

    iterator begin() { return iterator{coro}; }
    iterator end() { return iterator{coro, nullptr}; }
};

generator <int> counter(int start, int end, int step = 1) {
    for (int i = start; i <= end; i += step) {
        co_yield i;
    }
}

int main() {
    for (auto n : counter(1, 10, 2)) {
        std::cout << n << ' ';
    }
    // 输出: 1 3 5 7 9
}

核心点

  • co_yield 把值写入 promisecurrent_value,然后暂停。
  • generator::iterator 通过 resume() 继续执行,直到遇到下一个 co_yieldco_return

4. 实际应用场景

  1. 异步 I/O

    • co_awaitstd::futureboost::asio::awaitable 等库配合,简化异步操作链。
    • 示例:使用 asio::co_spawn 编写异步网络服务器。
  2. 生成器

    • 想实现像 Python yield 那样的惰性序列时,协程是天然的选择。
    • 适用于大数据流、图像处理等场景。
  3. 状态机

    • 将复杂的状态机拆解为若干协程段,减少嵌套与回调地狱。
    • 常见于游戏 AI、交互式 UI 等。
  4. 多线程任务调度

    • 协程与线程池配合,减少线程切换开销,实现高并发任务。

5. 常见坑与调试技巧

场景 常见错误 解决办法
异常传播 co_await 后抛异常导致协程未正常 destroy promise_type 中实现 unhandled_exception() 并确保调用方在 destroy() 前检查 done()
内存泄漏 协程句柄未销毁 在生成器对象中实现 ~generator() 或使用 std::unique_ptr 包装句柄。
无限循环 co_yield 位置不当导致 resume() 何时停止 确认 final_suspend() 返回 std::suspend_always 并在调用方检查 coro.done()
性能问题 协程创建/销毁频繁导致堆分配 对协程使用 suspend_alwayssuspend_never 适当调整挂起点,或在对象池中复用句柄。
调试难度 协程的内部状态难以可视化 使用 IDE 的 “Coroutine View” 或在 promise_type 中添加日志;也可以将协程拆分为多个小函数。

6. 结语

C++20 的协程为异步编程提供了强大且语义清晰的工具,能够让我们用更少的代码书写高并发、低延迟、低耦合的程序。虽然起步时可能会碰到一些实现细节上的坑,但一旦熟悉了 promise_typecoroutine_handle 的交互方式,你将能在多种场景中大显身手。

如果你想进一步深入,建议阅读:

  • 《C++20 协程指南》
  • 《C++ Concurrency in Action》第二版(协程章节)
  • Boost.Asio 官方文档(协程示例)

Happy coding!

发表评论