文章目录
- C++20无栈协程经典样例与底层原理详解
-
- 一、经典C++20协程样例代码
- 二、代码底层逻辑拆解
-
- [1. 协程创建:编译器自动生成堆状态对象](#1. 协程创建:编译器自动生成堆状态对象)
- [2. 协程恢复(resume):状态机切换执行片段](#2. 协程恢复(resume):状态机切换执行片段)
- [3. 无栈协程的"无栈"本质:复用主线程栈](#3. 无栈协程的“无栈”本质:复用主线程栈)
- [4. 编译器自动生成的核心代码(伪代码)](#4. 编译器自动生成的核心代码(伪代码))
- [5. 协程销毁:释放堆状态对象](#5. 协程销毁:释放堆状态对象)
- 三、C++无栈协程的核心特性(呼应之前的知识点)
- 四、总结
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:- 编译器自动调用
promise_type::yield_value(1),将1存入堆上的promise.value; yield_value返回std::suspend_always,协程暂停,暂停点ID更新为1;- 控制权回到主线程,主线程通过
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++无栈协程的核心特性(呼应之前的知识点)
结合之前聊的无栈协程特性,对照本例总结:
- 依赖编译器:编译器拆分协程函数为状态机片段、生成堆状态对象、插入状态序列化代码,这是纯运行时库无法实现的;
- 无独立栈 :全程复用主线程栈,
co_yield时将状态序列化到堆,恢复时反序列化到主线程栈; - 无法嵌套暂停 :若在
generate_numbers()中调用嵌套函数并在其中co_yield,编译器无法将嵌套函数的栈帧序列化到堆,会直接编译报错; - 轻量性 :堆状态对象仅包含
promise和暂停点ID,内存开销远低于有栈协程(本例仅几十字节)。
四、总结
核心知识点回顾
- C++20协程是典型无栈协程 :编译器将协程函数拆分为状态机,通过堆上的
promise_type保存状态,复用调用者栈执行; - 核心结构 :
promise_type是状态载体,coroutine_handle是控制句柄,二者由编译器自动绑定; - 生命周期:创建(堆分配状态对象)→ 暂停(状态序列化到堆)→ 恢复(状态反序列化到调用者栈)→ 销毁(释放堆对象);
- 底层本质:编译器将"暂停/恢复"语义转化为"状态机+堆状态序列化",这是无栈协程依赖编译器的核心原因。
关键对比(与有栈协程)
| 特性 | C++20无栈协程 | 有栈协程(如libco) |
|---|---|---|
| 栈依赖 | 复用调用者栈 | 独立动态栈(堆/mmap分配) |
| 编译器依赖 | 必须(状态机+序列化) | 无需(仅运行时上下文切换) |
| 嵌套暂停 | 不支持 | 支持 |
| 内存开销 | 字节级(堆状态对象) | KB级(独立栈) |
| 切换逻辑 | 状态序列化/反序列化 | 上下文切换(SP/PC/寄存器) |
这份样例覆盖了C++无栈协程的核心逻辑,若需要对比有栈协程的C实现(如libco),可以告诉我,我会补充对应的代码和原理。