【C++】栈的变奏:从C++20无栈协程源码出发,透视编译器如何生成状态机

文章目录

C++20无栈协程经典样例与底层原理详解

结合之前聊的无栈协程核心特性(依赖编译器、状态序列化、复用调用者栈),我会先给出一个可直接运行的C++20无栈协程样例,再从代码结构编译器自动生成的底层逻辑协程生命周期三个维度拆解,让你彻底理解C++协程的执行原理。

一、经典C++20协程样例代码

这个样例实现了一个"可生成整数序列"的协程,包含协程核心结构(promise_type/coroutine_handle)、暂停/恢复逻辑、状态传递,覆盖无栈协程的核心特性:

cpp 复制代码
#include <coroutine>
#include <iostream>
#include <memory>
#include <optional>

// -------------------------- 1. 协程返回类型(可等待对象) --------------------------
// 定义协程的返回类型,需包含coroutine_handle和promise_type的绑定
struct Generator {
    // 核心:协程句柄,用于控制协程的暂停/恢复/销毁(指向堆上的协程状态对象)
    using handle_type = std::coroutine_handle<>;

    // ---------------------- 2. Promise类型(协程状态载体) ----------------------
    // 编译器要求:返回类型必须包含promise_type,用于存储协程状态、传递数据
    struct promise_type {
        // 存储协程产生的值(跨暂停点保留,序列化到堆)
        std::optional<int> value;
        // 存储协程是否结束的标记
        bool done = false;

        // 【编译器自动调用】协程创建时第一个调用的函数:返回协程的返回对象(Generator)
        Generator get_return_object() {
            std::cout << "[Promise] 初始化协程返回对象\n";
            // 将当前协程的handle绑定到Generator
            return Generator{handle_type::from_promise(*this)};
        }

        // 【编译器自动调用】协程启动时的暂停策略:std::suspend_always=立即暂停
        std::suspend_always initial_suspend() {
            std::cout << "[Promise] 协程启动,立即暂停(等待resume)\n";
            return {}; // 让协程在入口处暂停,需手动resume才开始执行
        }

        // 【编译器自动调用】协程结束时的暂停策略:std::suspend_always=暂停(避免立即销毁)
        std::suspend_always final_suspend() noexcept {
            std::cout << "[Promise] 协程执行结束,标记完成\n";
            done = true;
            return {};
        }

        // 【编译器自动调用】处理co_yield:接收协程产生的值,暂停协程
        std::suspend_always yield_value(int val) {
            std::cout << "[Promise] 捕获co_yield值:" << val << ",暂停协程\n";
            // 将值存储到promise(堆上),跨暂停点保留
            value = val;
            return {}; // 暂停协程,等待下一次resume
        }

        // 【编译器自动调用】处理协程返回(co_return)
        void return_void() {
            std::cout << "[Promise] 协程无返回值,完成执行\n";
        }

        // 【编译器自动调用】处理协程内未捕获的异常
        void unhandled_exception() {
            std::terminate(); // 简单处理:终止程序
        }
    };

    // ---------------------- Generator类的核心方法 ----------------------
    // 构造函数:绑定协程句柄
    explicit Generator(handle_type h) : coro_handle_(h) {}

    // 析构函数:销毁协程句柄,释放堆上的状态对象
    ~Generator() {
        if (coro_handle_) {
            std::cout << "[Generator] 销毁协程句柄,释放堆状态对象\n";
            coro_handle_.destroy();
        }
    }

    // 禁用拷贝(避免重复销毁句柄),启用移动
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    Generator(Generator&& other) noexcept : coro_handle_(other.coro_handle_) {
        other.coro_handle_ = nullptr;
    }
    Generator& operator=(Generator&& other) noexcept {
        if (this != &other) {
            if (coro_handle_) coro_handle_.destroy();
            coro_handle_ = other.coro_handle_;
            other.coro_handle_ = nullptr;
        }
        return *this;
    }

    // 恢复协程执行,返回是否还有值
    bool resume() {
        if (!coro_handle_ || coro_handle_.done()) return false;
        std::cout << "[Generator] 恢复协程执行\n";
        coro_handle_.resume(); // 核心:恢复协程,执行到下一个暂停点/结束
        return !coro_handle_.done();
    }

    // 获取协程产生的值(从promise的堆状态中读取)
    int value() const {
        // promise():获取句柄绑定的promise对象(堆上)
        return coro_handle_.promise().value.value();
    }

private:
    handle_type coro_handle_; // 协程句柄,指向堆上的协程状态对象
};

// -------------------------- 3. 协程函数(业务逻辑) --------------------------
// 无栈协程函数:生成1~3的整数序列,每次co_yield暂停
Generator generate_numbers() {
    std::cout << "[协程] 开始执行,准备生成数字\n";
    co_yield 1; // 暂停点1:将1存入promise,暂停
    co_yield 2; // 暂停点2:将2存入promise,暂停
    co_yield 3; // 暂停点3:将3存入promise,暂停
    std::cout << "[协程] 数字生成完成,即将结束\n";
}

// -------------------------- 4. 主函数(协程调用者) --------------------------
int main() {
    std::cout << "===== 启动协程 =====\n";
    // 创建协程:编译器自动生成堆上的状态对象,协程在initial_suspend处暂停
    Generator gen = generate_numbers();

    // 循环恢复协程,获取值
    while (gen.resume()) {
        std::cout << "[主线程] 获取协程值:" << gen.value() << "\n\n";
    }

    std::cout << "\n===== 协程执行完毕 =====\n";
    return 0;
}

编译运行说明

  • 编译命令:需用支持C++20协程的编译器(如GCC 11+、Clang 14+),命令为 g++ coro_demo.cpp -o coro_demo -std=c++20

  • 运行输出:

    复制代码
    ===== 启动协程 =====
    [Promise] 初始化协程返回对象
    [Promise] 协程启动,立即暂停(等待resume)
    [Generator] 恢复协程执行
    [协程] 开始执行,准备生成数字
    [Promise] 捕获co_yield值:1,暂停协程
    [主线程] 获取协程值:1
    
    [Generator] 恢复协程执行
    [Promise] 捕获co_yield值:2,暂停协程
    [主线程] 获取协程值:2
    
    [Generator] 恢复协程执行
    [Promise] 捕获co_yield值:3,暂停协程
    [主线程] 获取协程值:3
    
    [Generator] 恢复协程执行
    [协程] 数字生成完成,即将结束
    [Promise] 协程无返回值,完成执行
    [Promise] 协程执行结束,标记完成
    
    ===== 协程执行完毕 =====
    [Generator] 销毁协程句柄,释放堆状态对象

二、代码底层逻辑拆解

C++20无栈协程的核心是编译器将协程函数改造成"状态机+堆状态对象",以下从5个核心维度讲解底层逻辑:

1. 协程创建:编译器自动生成堆状态对象

当调用 generate_numbers() 时,编译器不会直接执行函数,而是:

  • 步骤1 :在堆上分配一块内存(协程状态对象),包含:
    • promise_type 对象(存储 value/done 等状态);
    • 协程的暂停点ID(标记当前执行到哪个 co_yield);
    • 协程函数的局部变量(本例中无,若有则序列化到此处);
    • 编译器生成的状态机元信息(执行片段入口地址)。
  • 步骤2 :调用 promise_type::get_return_object(),返回 Generator 对象,绑定协程句柄(指向堆状态对象);
  • 步骤3 :执行 promise_type::initial_suspend(),协程在入口处暂停,等待 resume() 触发执行。

2. 协程恢复(resume):状态机切换执行片段

gen.resume() 调用 coro_handle_.resume() 时,编译器驱动协程按"状态机"执行:

  • 初始状态:暂停点ID=0(未执行),恢复后执行到第一个 co_yield 1
  • 执行 co_yield 1
    1. 编译器自动调用 promise_type::yield_value(1),将1存入堆上的 promise.value
    2. yield_value 返回 std::suspend_always,协程暂停,暂停点ID更新为1;
    3. 控制权回到主线程,主线程通过 gen.value() 从堆上的 promise 读取值。
  • 再次 resume():暂停点ID=1,恢复后执行到 co_yield 2,重复上述流程,直到所有暂停点执行完毕。

3. 无栈协程的"无栈"本质:复用主线程栈

generate_numbers() 的执行全程复用主线程栈

  • 每次 resume() 时,编译器将堆状态对象中的变量(如 promise.value)拷贝到主线程栈,执行当前片段;
  • 执行到 co_yield 时,将主线程栈上的状态(本例仅 value)拷贝回堆状态对象,销毁主线程栈上的协程栈帧;
  • 恢复时再次将堆状态拷贝到主线程栈,重建栈帧执行下一个片段。

4. 编译器自动生成的核心代码(伪代码)

编译器会将 generate_numbers() 改造成如下状态机(开发者无需编写,纯编译期生成):

cpp 复制代码
// 编译器自动生成的协程状态机(伪代码)
struct __generate_numbers_state {
    // 1. 堆上的promise对象(状态载体)
    Generator::promise_type promise;
    // 2. 暂停点ID(状态机状态)
    int resume_point = 0;

    // 3. 状态机执行函数
    void resume() {
        switch (resume_point) {
            case 0: // 初始状态:执行到第一个co_yield
                promise.value = 1;
                resume_point = 1; // 更新暂停点
                return; // 暂停,回到调用者
            case 1: // 恢复后执行到第二个co_yield
                promise.value = 2;
                resume_point = 2;
                return;
            case 2: // 恢复后执行到第三个co_yield
                promise.value = 3;
                resume_point = 3;
                return;
            case 3: // 恢复后执行到结束
                resume_point = -1; // 标记结束
                promise.return_void();
                return;
        }
    }
};

// 编译器改造后的generate_numbers()
Generator generate_numbers() {
    // 1. 堆上分配状态对象
    auto* state = new __generate_numbers_state;
    // 2. 绑定promise和句柄
    auto handle = std::coroutine_handle<Generator::promise_type>::from_promise(state->promise);
    // 3. 执行initial_suspend,返回Generator
    state->promise.initial_suspend();
    return Generator{handle};
}

5. 协程销毁:释放堆状态对象

Generator 析构时调用 coro_handle_.destroy(),编译器自动:

  • 调用 promise_type::final_suspend(),标记协程结束;
  • 释放堆上的 __generate_numbers_state 内存(包含promise、暂停点、局部变量);
  • 若不手动销毁,会导致堆内存泄漏(无栈协程的资源管理需开发者关注)。

三、C++无栈协程的核心特性(呼应之前的知识点)

结合之前聊的无栈协程特性,对照本例总结:

  1. 依赖编译器:编译器拆分协程函数为状态机片段、生成堆状态对象、插入状态序列化代码,这是纯运行时库无法实现的;
  2. 无独立栈 :全程复用主线程栈,co_yield 时将状态序列化到堆,恢复时反序列化到主线程栈;
  3. 无法嵌套暂停 :若在 generate_numbers() 中调用嵌套函数并在其中 co_yield,编译器无法将嵌套函数的栈帧序列化到堆,会直接编译报错;
  4. 轻量性 :堆状态对象仅包含 promise 和暂停点ID,内存开销远低于有栈协程(本例仅几十字节)。

四、总结

核心知识点回顾

  1. C++20协程是典型无栈协程 :编译器将协程函数拆分为状态机,通过堆上的 promise_type 保存状态,复用调用者栈执行;
  2. 核心结构promise_type 是状态载体,coroutine_handle 是控制句柄,二者由编译器自动绑定;
  3. 生命周期:创建(堆分配状态对象)→ 暂停(状态序列化到堆)→ 恢复(状态反序列化到调用者栈)→ 销毁(释放堆对象);
  4. 底层本质:编译器将"暂停/恢复"语义转化为"状态机+堆状态序列化",这是无栈协程依赖编译器的核心原因。

关键对比(与有栈协程)

特性 C++20无栈协程 有栈协程(如libco)
栈依赖 复用调用者栈 独立动态栈(堆/mmap分配)
编译器依赖 必须(状态机+序列化) 无需(仅运行时上下文切换)
嵌套暂停 不支持 支持
内存开销 字节级(堆状态对象) KB级(独立栈)
切换逻辑 状态序列化/反序列化 上下文切换(SP/PC/寄存器)

这份样例覆盖了C++无栈协程的核心逻辑,若需要对比有栈协程的C实现(如libco),可以告诉我,我会补充对应的代码和原理。

相关推荐
安科士andxe8 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
寻寻觅觅☆11 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc11 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
小白同学_C11 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖11 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
儒雅的晴天12 小时前
大模型幻觉问题
运维·服务器
ceclar12312 小时前
C++使用format
开发语言·c++·算法
通信大师13 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
lanhuazui1013 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4413 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索