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

在 C++20 之前,异步编程通常依赖于回调、线程、事件循环等方式,开发者需要自己手动管理状态机、上下文切换等细节。C++20 引入协程(Coroutines)后,这一切变得更加自然。本文将从协程的基本概念、实现原理、标准库支持以及实际编码示例,帮助你快速掌握并落地协程技术。

1. 协程到底是什么?

协程是一种轻量级的“用户级线程”,它们可以在执行过程中暂停(yield)并恢复,保持自己的执行状态。与传统线程不同,协程没有自己的堆栈;它们通过编译器生成的状态机来保存局部变量、指令指针等信息。协程使得写出顺序式的异步代码成为可能,提升代码可读性。

2. 关键概念

名词 含义
co_await 等待一个 awaitable 对象完成,暂停协程
co_yield 产生一个值,暂停协程
co_return 返回协程结果,结束协程
promise_type 协程的“承诺”类型,负责协程的生命周期管理
awaitable 可等待的对象,满足 await_readyawait_suspendawait_resume 这三个成员函数

3. 标准库支持

C++20 的标准库为协程提供了 std::suspend_alwaysstd::suspend_neverstd::coroutine_handlestd::experimental::generator 等基础设施。但 std::generator 仍属于实验性(std::experimental)模块。到 C++23,实验性模块将被正式纳入标准。

4. 一个简单的协程示例:生成斐波那契数列

#include <iostream>
#include <coroutine>
#include <exception>
#include <vector>

// 简易 generator
template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::exception_ptr exception;

        generator get_return_object() {
            return generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) noexcept {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() {
            exception = std::current_exception();
        }
    };

    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> h;
        bool done;

        iterator(std::coroutine_handle <promise_type> h, bool done) : h(h), done(done) {}

        iterator& operator++() {
            h.resume();
            done = h.done();
            return *this;
        }
        const T& operator*() const { return h.promise().current_value; }
        bool operator!=(const iterator& other) const { return done != other.done; }
    };

    iterator begin() {
        if (coro) {
            coro.resume();
            if (coro.done()) return iterator{coro, true};
        }
        return iterator{coro, false};
    }
    iterator end() { return iterator{coro, true}; }
};

generator <int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int next = a + b;
        a = b; b = next;
    }
}
int main() {
    for (int x : fibonacci(10)) {
        std::cout << x << ' ';
    }
    // 输出: 0 1 1 2 3 5 8 13 21 34
}

上述代码演示了如何使用 co_yield 产生一个可迭代的序列,并通过 generator 封装成标准容器的接口。协程内部的状态(ab 等)在每次 co_yield 后被安全地保存在 promise_type 中。

5. 实际应用场景

场景 协程优势
异步 I/O co_await 代替回调链,逻辑更直观
事件驱动 co_yield 产生事件,事件循环简单化
流式数据 generator 以惰性方式生成数据,节省内存
协程池 通过手动管理 coroutine_handle 构建轻量级协程池

6. 性能考虑

  • 堆栈开销:协程的堆栈由编译器自动分配,通常比线程小得多(数十 KB)。这使得数千协程可以同时存在,而线程数量受限于系统堆栈大小。
  • 切换成本:协程切换是 CPU 指令级别的,远比线程切换快。只要协程暂停时不执行同步锁,竞争也几乎不存在。
  • 内存分配:若协程内部使用大量 new,仍会产生分配开销。建议使用预分配或对象池来避免频繁分配。

7. 小结

C++20 的协程为异步编程带来了新的维度,让你在保持代码顺序式结构的同时,享受高效的并发性能。只需掌握 co_awaitco_yieldco_return 的语义,并利用标准库提供的协程框架,你就能快速构建出可维护、可扩展的异步系统。欢迎在项目中大胆试验,把协程真正落地到生产环境中!

发表评论