文章目录
- C++协程的代码逻辑
-
-
- 一、先理解C++20协程的核心底层模型
- 二、逐模块解析底层逻辑
-
- [1. 协程返回类型`Generator`:为什么必须包含`promise_type`?](#1. 协程返回类型
Generator:为什么必须包含promise_type?) - [2. `promise_type`:为什么要实现这些成员函数?](#2.
promise_type:为什么要实现这些成员函数?) - [3. `Generator`的核心方法:`resume()`和`value()`](#3.
Generator的核心方法:resume()和value()) - [4. 析构函数:为什么要调用`coro_handle_.destroy()`?](#4. 析构函数:为什么要调用
coro_handle_.destroy()?) - [5. 协程函数`generate_numbers()`:为什么用`co_yield`?](#5. 协程函数
generate_numbers():为什么用co_yield?)
- [1. 协程返回类型`Generator`:为什么必须包含`promise_type`?](#1. 协程返回类型
- 三、协程执行的完整底层流程(对应main函数)
- 总结(核心底层要点)
-
- [详细介绍Awaitable 类型](#详细介绍Awaitable 类型)
-
-
- [一、Awaitable 类型的核心定义](#一、Awaitable 类型的核心定义)
- [二、Awaitable 类型的核心规范(3个必须实现的接口)](#二、Awaitable 类型的核心规范(3个必须实现的接口))
- [三、Awaitable 类型的两种实现方式](#三、Awaitable 类型的两种实现方式)
-
- 方式1:自身就是awaiter(最常用)
- [方式2:通过`operator co_await`生成awaiter](#方式2:通过
operator co_await生成awaiter)
- [四、Awaitable 类型的核心分类(按用途)](#四、Awaitable 类型的核心分类(按用途))
-
- [1. 基础暂停型(返回void)](#1. 基础暂停型(返回void))
- [2. 自动恢复型(返回coroutine_handle)](#2. 自动恢复型(返回coroutine_handle))
- [3. 取消暂停型(返回bool)](#3. 取消暂停型(返回bool))
- [五、Awaitable 类型的实际应用例子(异步任务)](#五、Awaitable 类型的实际应用例子(异步任务))
- [总结(Awaitable 类型核心要点)](#总结(Awaitable 类型核心要点))
-
- Awaitable类型的设计初衷
-
-
- [一、Awaitable 类型的设计初衷:解决"暂停-等待-恢复"的通用化问题](#一、Awaitable 类型的设计初衷:解决“暂停-等待-恢复”的通用化问题)
-
- [1. 为什么需要"通用化"?](#1. 为什么需要“通用化”?)
- [2. 3个核心接口的设计逻辑(为什么是这3个函数?)](#2. 3个核心接口的设计逻辑(为什么是这3个函数?))
- 二、理解std::suspend_always/never:极简的Awaitable"基础款"
-
- [1. 先看标准库的核心实现(简化版)](#1. 先看标准库的核心实现(简化版))
- [2. 为什么要设计这两个类型?](#2. 为什么要设计这两个类型?)
- [3. 结合之前的Generator代码理解](#3. 结合之前的Generator代码理解)
- 三、理解co_yield:Awaitable的"语法糖"
-
- [1. co_yield的编译展开(底层本质)](#1. co_yield的编译展开(底层本质))
- [2. 为什么要设计co_yield?](#2. 为什么要设计co_yield?)
- [3. 结合代码理解co_yield的执行流程](#3. 结合代码理解co_yield的执行流程)
- 总结(核心设计逻辑)
-
C++协程的代码逻辑
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无栈协程代码为什么要这么设计和编写,核心是搞清楚协程状态管理、暂停/恢复机制、数据传递的底层逻辑。
一、先理解C++20协程的核心底层模型
C++20协程本质是无栈协程(Stackless Coroutine),其底层核心原理可以概括为:
- 协程启动时,编译器会将协程的状态(局部变量、暂停点、指令指针) 打包成一个堆上的状态对象(而非栈帧);
- 协程的暂停(
co_yield/co_await)本质是「保存当前指令指针+释放调用栈」,恢复(resume)是「恢复指令指针+重新占用调用栈」; - 协程的返回类型(如
Generator)必须通过promise_type与堆上的状态对象绑定,promise_type是协程状态的"载体",负责数据传递和生命周期管理。
下面结合代码逐模块拆解"为什么要这么写":
二、逐模块解析底层逻辑
1. 协程返回类型Generator:为什么必须包含promise_type?
底层原理 :
C++编译器对协程有强制要求:协程的返回类型必须嵌套定义promise_type,因为:
- 协程启动时,编译器会先创建
promise_type对象(作为协程状态的核心部分,分配在堆上); - 所有跨暂停点的数据(如
co_yield的值)、协程的暂停/恢复策略,都由promise_type管理; Generator作为"协程控制器",需要通过std::coroutine_handle绑定到堆上的promise_type,从而实现对协程的控制(恢复、销毁、读取数据)。
代码设计的底层原因:
cpp
struct Generator {
using handle_type = std::coroutine_handle<>; // 绑定到promise_type的句柄
struct promise_type { ... }; // 编译器强制要求的协程状态载体
handle_type coro_handle_; // 指向堆上协程状态的句柄
};
coroutine_handle:是协程的"底层句柄",本质是一个指针,指向堆上的协程状态对象(包含promise_type、指令指针、局部变量等);- 禁用拷贝、启用移动:因为
coroutine_handle指向唯一的堆状态对象,拷贝会导致重复销毁(双重释放),移动是唯一安全的传递方式。
2. promise_type:为什么要实现这些成员函数?
promise_type的所有成员函数都是编译器自动调用的,对应协程生命周期的关键节点,缺一不可:
| 成员函数 | 底层调用时机 | 为什么要这么实现? |
|---|---|---|
get_return_object() |
协程创建时第一个调用 | 返回Generator对象,并将当前协程的handle绑定到Generator,让调用者能控制协程; |
initial_suspend() |
协程启动后(执行第一行代码前) | 返回std::suspend_always:让协程"启动即暂停",等待调用者手动resume(避免协程一次性执行完); |
final_suspend() |
协程执行结束时 | 返回std::suspend_always:避免协程结束后立即销毁堆状态对象,等待Generator析构时手动销毁; |
yield_value(int val) |
遇到co_yield val时 |
接收co_yield的值并存储到promise_type(堆上),然后暂停协程(跨暂停点保留数据); |
return_void() |
协程无co_return时自动调用 |
标记协程无返回值,完成执行; |
unhandled_exception() |
协程内抛出未捕获异常时 | 处理异常(这里简单终止程序,避免内存泄漏); |
核心底层细节:
std::suspend_always/std::suspend_never:是C++20提供的"暂停策略",本质是包含await_ready()/await_suspend()/await_resume()的空结构体;yield_value返回suspend_always:让协程在co_yield处暂停,此时堆上的promise_type保存了val,调用者可以通过Generator读取;promise_type的value用std::optional:因为协程启动时还未产生值,optional可以安全处理"无值"状态。
3. Generator的核心方法:resume()和value()
resume()的底层逻辑:
cpp
bool resume() {
if (!coro_handle_ || coro_handle_.done()) return false;
coro_handle_.resume(); // 核心:恢复协程执行
return !coro_handle_.done();
}
coro_handle_.resume():底层是"恢复协程的指令指针",让协程从上次暂停的位置继续执行,直到下一个co_yield或协程结束;coro_handle_.done():判断协程是否执行完毕(指令指针到达协程末尾),避免重复恢复。
value()的底层逻辑:
cpp
int value() const {
return coro_handle_.promise().value.value();
}
coro_handle_.promise():底层是通过句柄指针,直接访问堆上的promise_type对象;- 读取
promise_type的value:因为promise_type在堆上,跨暂停点不会销毁,所以能安全读取co_yield的值。
4. 析构函数:为什么要调用coro_handle_.destroy()?
cpp
~Generator() {
if (coro_handle_) {
coro_handle_.destroy();
coro_handle_ = nullptr;
}
}
底层原因:
- 协程的状态对象(包含
promise_type、指令指针、局部变量)分配在堆上,编译器不会自动释放; coro_handle_.destroy():是唯一释放堆上协程状态的方式,若不调用会导致内存泄漏;- 析构时检查
coro_handle_是否为空:避免空指针调用destroy()。
5. 协程函数generate_numbers():为什么用co_yield?
cpp
Generator generate_numbers() {
co_yield 1;
co_yield 2;
co_yield 3;
}
底层原理:
co_yield val:是C++20的语法糖,等价于co_await promise.yield_value(val);- 每次
co_yield:编译器会调用promise_type::yield_value(val),将值存入堆上的promise_type,然后暂停协程,释放调用栈; - 协程的局部变量(若有):会被编译器打包到堆上的状态对象中,跨暂停点保留。
三、协程执行的完整底层流程(对应main函数)
cpp
Generator gen = generate_numbers(); // 协程创建
while (gen.resume()) { ... } // 循环恢复协程
- 创建协程 :
- 编译器分配堆上的协程状态对象(包含
promise_type、指令指针、局部变量); - 调用
promise.get_return_object(),返回绑定了句柄的Generator; - 调用
promise.initial_suspend(),协程在入口处暂停(未执行任何业务逻辑)。
- 编译器分配堆上的协程状态对象(包含
- 第一次
gen.resume():- 恢复协程,执行到
co_yield 1; - 调用
promise.yield_value(1),将1存入堆上的promise_type,协程暂停; - 主线程读取
gen.value(),获取堆上的1。
- 恢复协程,执行到
- 第二次
gen.resume():- 恢复协程,从
co_yield 1的位置继续执行到co_yield 2; - 重复上述流程,存入2并暂停。
- 恢复协程,从
- 第三次
gen.resume():- 恢复协程,执行到
co_yield 3,存入3并暂停。
- 恢复协程,执行到
- 第四次
gen.resume():- 恢复协程,执行完剩余代码(
"数字生成完成"),调用promise.return_void(); - 调用
promise.final_suspend(),协程标记为done; gen.resume()返回false,循环结束。
- 恢复协程,执行完剩余代码(
gen析构 :- 调用
coro_handle_.destroy(),释放堆上的协程状态对象。
- 调用
总结(核心底层要点)
- 堆上状态管理 :C++20协程的核心是将状态(数据+指令指针)分配在堆上,而非栈上,因此必须通过
promise_type存储跨暂停点的数据,通过coroutine_handle控制状态; - 编译器强制约定 :
promise_type的成员函数(get_return_object/initial_suspend等)是编译器的"强制接口",必须实现,否则无法编译,这些函数对应协程生命周期的关键节点; - 手动生命周期控制 :协程的堆状态不会自动释放,必须通过
coroutine_handle_.destroy()手动销毁(通常在返回类型的析构函数中),否则会内存泄漏; - 暂停/恢复的本质 :
co_yield/resume是"保存指令指针+释放栈"和"恢复指令指针+占用栈"的过程,堆上的promise_type是数据传递的桥梁。
这段代码的每一个细节,都是为了适配C++20协程的底层模型------通过promise_type管理堆状态,通过coroutine_handle控制协程,通过暂停/恢复实现"分段执行"。
详细介绍Awaitable 类型
深入理解C++20协程中的Awaitable(可等待对象) 类型,这是协程暂停/恢复机制的核心,从定义、核心接口、底层原理、分类和实际例子几个维度,把这个概念讲透。
一、Awaitable 类型的核心定义
Awaitable(可等待对象) 是满足C++20协程标准的一种类型,本质是:
任何能被
co_await运算符作用的对象,都称为"Awaitable类型"。
简单来说,co_await expr能否编译通过,完全取决于expr的类型是否是Awaitable------这是协程实现"暂停等待某个条件,条件满足后恢复"的核心载体(比如co_yield val本质就是co_await promise.yield_value(val),而yield_value的返回值就是Awaitable类型)。
底层逻辑:co_await的编译展开
写co_await awaitable时,编译器会自动将其展开为以下逻辑(伪代码):
cpp
// 1. 获取awaiter(等待器):awaitable的核心执行体
auto&& awaiter = get_awaitable(awaitable);
// 2. 检查是否需要暂停:若ready=true,直接返回结果,不暂停
if (!awaiter.await_ready()) {
// 3. 暂停协程:保存协程状态,将句柄传给awaiter,释放调用栈
awaiter.await_suspend(coroutine_handle);
}
// 4. 恢复后执行:获取等待结果(无论是否暂停,最终都会执行)
auto result = awaiter.await_resume();
这个展开逻辑也决定了:Awaitable类型的核心是提供一个符合规范的awaiter(等待器)。
二、Awaitable 类型的核心规范(3个必须实现的接口)
Awaitable类型的本质是"能生成符合规范的awaiter",而awaiter必须实现以下3个成员函数(缺一不可,编译器强制要求):
| 成员函数 | 返回值类型 | 核心作用 | 调用时机 |
|---|---|---|---|
await_ready() |
bool |
判断是否需要暂停协程: - true:无需暂停,直接执行await_resume() - false:需要暂停,执行await_suspend() |
co_await执行的第一步 |
await_suspend(handle) |
void/bool/std::coroutine_handle<> |
协程暂停时的逻辑: - 保存协程句柄、注册回调(比如异步任务完成后恢复协程) - 返回值控制是否真正暂停 | await_ready()返回false时 |
await_resume() |
任意类型(或void) | 协程恢复后的逻辑: - 返回等待的结果(比如异步任务的返回值、co_yield的值) |
协程恢复后(或未暂停时) |
关键补充:
await_suspend的参数:必须是std::coroutine_handle<Promise>(协程句柄),用于操作当前暂停的协程;- 接口的访问权限:这3个函数可以是
public,也可以是private(编译器能绕过访问控制调用); - 返回值灵活度:
await_resume的返回值会成为co_await awaitable整个表达式的结果。
三、Awaitable 类型的两种实现方式
C++20允许两种方式定义Awaitable类型,本质都是为了生成符合规范的awaiter:
方式1:自身就是awaiter(最常用)
直接在类型中实现await_ready()、await_suspend()、await_resume(),此时该类型既是Awaitable,也是awaiter。
C++标准库提供的std::suspend_always/std::suspend_never就是典型例子:
cpp
// 标准库实现(简化版)
struct suspend_always {
// 总是需要暂停
constexpr bool await_ready() const noexcept { return false; }
// 暂停时不做额外操作,直接返回void(表示真正暂停)
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
// 恢复后无返回值
constexpr void await_resume() const noexcept {}
};
struct suspend_never {
// 从不暂停
constexpr bool await_ready() const noexcept { return true; }
// 不会被调用(因为await_ready返回true)
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
// 无返回值
constexpr void await_resume() const noexcept {}
};
之前代码中的initial_suspend()/final_suspend()/yield_value()都返回suspend_always,就是利用这种方式让协程暂停。
方式2:通过operator co_await生成awaiter
如果类型本身不想实现3个核心接口,可以重载operator co_await,让这个运算符返回一个符合规范的awaiter。
这种方式适合"给已有类型(如std::future)增加Awaitable能力",示例:
cpp
// 假设有一个简单的Future类型
template <typename T>
struct Future {
T value;
bool ready = false;
// 重载operator co_await,返回awaiter
auto operator co_await() {
// 定义匿名awaiter(实现3个核心接口)
struct Awaiter {
Future& future; // 绑定外部的Future
std::coroutine_handle<> handle; // 保存协程句柄
// 检查Future是否已就绪
bool await_ready() const noexcept {
return future.ready;
}
// 暂停协程,保存句柄(比如注册回调:Future就绪后恢复协程)
void await_suspend(std::coroutine_handle<> h) noexcept {
handle = h;
// 模拟:异步任务完成后,调用handle.resume()恢复协程
}
// 恢复后返回Future的值
T await_resume() noexcept {
return future.value;
}
};
return Awaiter{*this}; // 返回awaiter
}
};
// 此时Future就是Awaitable类型,可以直接co_await
Generator use_future() {
Future<int> f;
int val = co_await f; // 合法,因为Future重载了operator co_await
co_yield val;
}
四、Awaitable 类型的核心分类(按用途)
根据await_suspend的返回值和使用场景,Awaitable类型主要分为3类,覆盖协程的核心用法:
1. 基础暂停型(返回void)
- 特点:
await_suspend返回void,表示"协程一定会暂停,且暂停后需要外部手动恢复"; - 典型例子:
std::suspend_always、之前代码中yield_value返回的对象; - 适用场景:手动控制协程暂停/恢复(如Generator生成器)。
2. 自动恢复型(返回coroutine_handle)
-
特点:
await_suspend返回另一个协程句柄,编译器会自动恢复该句柄指向的协程; -
适用场景:协程间切换(比如A协程暂停后,自动恢复B协程执行);
-
示例:
cppstruct SwitchCoroutine { std::coroutine_handle<> target_handle; bool await_ready() const noexcept { return false; } // 返回目标协程句柄,编译器会自动resume这个句柄 std::coroutine_handle<> await_suspend(std::coroutine_handle<>) noexcept { return target_handle; } void await_resume() const noexcept {} };
3. 取消暂停型(返回bool)
-
特点:
await_suspend返回bool:true:正常暂停(需要外部恢复);false:取消暂停,立即执行await_resume();
-
适用场景:动态决定是否暂停(比如根据运行时条件);
-
示例:
cppstruct ConditionalSuspend { bool should_suspend; bool await_ready() const noexcept { return false; } bool await_suspend(std::coroutine_handle<>) noexcept { return should_suspend; // true=暂停,false=取消暂停 } void await_resume() const noexcept {} };
五、Awaitable 类型的实际应用例子(异步任务)
下面用一个完整的例子,展示Awaitable类型如何实现"异步任务完成后自动恢复协程",这也是Awaitable最核心的实际用途:
cpp
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>
// 1. 定义异步任务的返回类型(协程返回类型)
struct AsyncTask {
struct promise_type {
int result; // 存储异步任务的结果
std::coroutine_handle<> handle; // 保存协程句柄
AsyncTask get_return_object() {
return AsyncTask{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(int val) { result = val; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> coro_handle;
};
// 2. 定义Awaitable类型:异步等待1秒后返回结果
struct SleepAwaitable {
int sleep_ms;
// 实现awaiter接口
bool await_ready() const noexcept {
// 若睡眠时间为0,无需暂停
return sleep_ms == 0;
}
void await_suspend(std::coroutine_handle<> handle) noexcept {
// 启动新线程:睡眠指定时间后,恢复协程
std::thread([handle, ms = sleep_ms]() {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
std::cout << "[异步线程] 睡眠完成,恢复协程\n";
handle.resume(); // 核心:自动恢复暂停的协程
}).detach();
}
int await_resume() const noexcept {
// 恢复后返回结果
return sleep_ms;
}
};
// 3. 协程函数:使用Awaitable类型
AsyncTask async_operation() {
std::cout << "[协程] 开始执行,准备睡眠1秒\n";
// co_await SleepAwaitable:暂停协程,异步睡眠后自动恢复
int ms = co_await SleepAwaitable{1000};
std::cout << "[协程] 恢复执行,睡眠了" << ms << "ms\n";
co_return ms; // 返回结果
}
int main() {
AsyncTask task = async_operation();
// 等待异步任务完成(简化版,实际需更安全的同步)
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
std::cout << "[主线程] 异步任务结果:" << task.coro_handle.promise().result << std::endl;
task.coro_handle.destroy();
return 0;
}
执行流程解析:
- 调用
async_operation(),协程启动,执行到co_await SleepAwaitable{1000}; - 调用
SleepAwaitable::await_ready(),返回false(需要暂停); - 调用
await_suspend():启动新线程睡眠1秒,同时协程暂停,释放主线程; - 1秒后,异步线程调用
handle.resume(),恢复协程; - 协程执行
await_resume(),获取返回值1000,最终执行co_return。
总结(Awaitable 类型核心要点)
- 核心本质 :Awaitable是能被
co_await作用的类型,其核心是提供符合规范的awaiter(实现await_ready/await_suspend/await_resume); - 编译逻辑 :
co_await会自动展开为"检查是否暂停→暂停处理→恢复获取结果"三步,完全依赖awaiter的3个接口; - 实现方式 :要么自身实现3个接口(如
suspend_always),要么重载operator co_await返回awaiter; - 核心价值:是协程"暂停等待"的载体,既能实现手动控制(如Generator),也能实现自动恢复(如异步任务)。
理解Awaitable类型,就理解了C++20协程暂停/恢复的底层逻辑------所有co_await/co_yield的行为,最终都由Awaitable类型的awaiter接口决定。
Awaitable类型的设计初衷
C++20协程中Awaitable类型的设计初衷 、std::suspend_always/never的作用,以及co_yield的底层本质------核心是搞清楚这些设计是如何服务于"协程暂停/恢复"这一核心需求的。
一、Awaitable 类型的设计初衷:解决"暂停-等待-恢复"的通用化问题
C++20设计Awaitable类型的核心目标,是为协程的暂停/恢复提供一套通用、可扩展的接口,而非硬编码死暂停逻辑。
1. 为什么需要"通用化"?
协程的暂停场景千差万别:
- 有的场景需要"启动即暂停"(如Generator生成器,等调用者手动恢复);
- 有的场景需要"等待异步任务完成后自动恢复"(如网络请求、文件IO);
- 有的场景需要"条件满足时才暂停"(如根据运行时参数决定是否暂停)。
如果编译器直接硬编码暂停逻辑,就无法适配这些差异化场景。因此,C++20将"是否暂停、暂停时做什么、恢复后返回什么"的逻辑,剥离到用户自定义的Awaitable类型中,让编译器只负责执行通用流程,具体逻辑由用户控制。
2. 3个核心接口的设计逻辑(为什么是这3个函数?)
Awaitable的await_ready/await_suspend/await_resume是对"暂停-恢复"流程的最小化抽象:
渲染错误: Mermaid 渲染失败: Parse error on line 2: ... --> B[调用await_ready()] B -->|返回true -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
await_ready():前置判断,避免无意义的暂停(比如异步任务已经完成,就没必要暂停协程);await_suspend():暂停时的核心逻辑,负责保存协程句柄、注册恢复回调(这是"等待"的核心);await_resume():恢复后的收尾 ,返回等待结果(比如异步任务的返回值、co_yield的值)。
这套设计既满足了"必须暂停"(如Generator)、"永不暂停"(如同步任务)、"条件暂停"(如动态判断)等基础场景,也支持"异步自动恢复"(如网络IO)等复杂场景------通用性拉满,且无冗余。
二、理解std::suspend_always/never:极简的Awaitable"基础款"
std::suspend_always和std::suspend_never是C++标准库提供的最简化Awaitable类型,专门用于处理"无条件暂停"和"无条件不暂停"这两种最基础的场景。
1. 先看标准库的核心实现(简化版)
cpp
// 无条件暂停:协程一定会暂停,且暂停后无额外逻辑
struct suspend_always {
// 必须暂停
constexpr bool await_ready() const noexcept { return false; }
// 暂停时不做任何操作(只需保存协程状态,等外部手动恢复)
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
// 恢复后无返回值
constexpr void await_resume() const noexcept {}
};
// 无条件不暂停:协程永不暂停,直接执行后续逻辑
struct suspend_never {
// 无需暂停
constexpr bool await_ready() const noexcept { return true; }
// 永远不会被调用(因为await_ready返回true)
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
// 无返回值
constexpr void await_resume() const noexcept {}
};
2. 为什么要设计这两个类型?
它们是"通用接口的极简实现",解决了协程生命周期中两个核心的默认需求:
| 场景 | 使用哪个? | 核心作用 |
|---|---|---|
| 协程启动时暂停(如Generator) | suspend_always | 让协程"创建后不立即执行",等待调用者手动resume()(避免协程一次性跑完); |
| 协程启动时立即执行(如异步任务) | suspend_never | 让协程"创建后直接执行",直到遇到第一个co_await/co_yield才暂停; |
| 协程结束时暂停 | suspend_always | 避免协程结束后立即销毁堆状态对象,等待用户手动释放(防止内存泄漏); |
3. 结合之前的Generator代码理解
cpp
// 协程启动时立即暂停,等调用者resume()
std::suspend_always initial_suspend() { return {}; }
// 协程结束时暂停,等析构函数destroy()
std::suspend_always final_suspend() noexcept { return {}; }
// co_yield时暂停,保存值后等下一次resume()
std::suspend_always yield_value(int val) { return {}; }
这里全部返回suspend_always,本质是:
- 协程的所有暂停点(启动、yield、结束)都采用"无条件暂停+手动恢复"的逻辑;
- 这是Generator生成器的核心需求:调用者通过
resume()控制协程的执行节奏,逐次获取值。
三、理解co_yield:Awaitable的"语法糖"
co_yield是C++20为"生成器场景"提供的语法糖 ,其底层完全依赖Awaitable类型实现------本质是co_await promise.yield_value(val)的简写。
1. co_yield的编译展开(底层本质)
当写:
cpp
co_yield 1;
编译器会自动将其展开为:
cpp
co_await promise.yield_value(1);
整个流程拆解:
- 调用协程对应的
promise_type::yield_value(1),传入要生成的值; yield_value返回一个Awaitable类型(比如代码中的suspend_always);- 执行
co_await逻辑:调用该Awaitable的await_ready()(返回false)→ 调用await_suspend()(暂停协程)→ 等待恢复后调用await_resume(); - 在此过程中,
yield_value会将值存入promise_type(堆上),供调用者读取。
2. 为什么要设计co_yield?
- 简化生成器代码 :生成器是协程最基础的应用场景(逐次生成值),如果每次都写
co_await promise.yield_value(val),代码会非常冗余; - 语义化 :
co_yield直接表达"产出一个值并暂停"的语义,比co_await更贴合生成器的业务逻辑; - 与Awaitable解耦 :
co_yield不关心yield_value返回的具体Awaitable类型,只需符合规范------可以返回suspend_always(手动恢复),也可以返回自定义Awaitable(比如"自动恢复"的生成器)。
3. 结合代码理解co_yield的执行流程
cpp
// 协程函数中的co_yield 1
co_yield 1;
// 编译器展开后:
co_await promise.yield_value(1);
// 具体执行步骤:
1. 调用promise.yield_value(1):
- 打印"捕获co_yield值:1,暂停协程";
- 将1存入promise的value(堆上);
- 返回suspend_always对象(Awaitable)。
2. 执行co_await suspend_always:
- 调用await_ready() → 返回false(需要暂停);
- 调用await_suspend() → 无操作,协程暂停;
- 等待调用者resume()后,调用await_resume() → 无返回值。
3. 协程暂停,主线程可以通过gen.value()读取堆上的1。
总结(核心设计逻辑)
- Awaitable的设计初衷:将协程"暂停/恢复"的逻辑从编译器中剥离,交给用户自定义,实现通用化、可扩展的暂停策略(适配手动控制、异步自动恢复等所有场景);
- std::suspend_always/never的作用:是Awaitable的"极简基础实现",覆盖"无条件暂停"和"无条件不暂停"两种核心场景,避免用户重复编写冗余代码;
- co_yield的本质 :是
co_await promise.yield_value(val)的语法糖,专门简化生成器场景的代码,其暂停逻辑完全依赖yield_value返回的Awaitable类型。
这些设计的核心逻辑是:编译器只负责协程的通用生命周期管理,具体的暂停/恢复策略由用户通过Awaitable类型定义------这也是C++20协程"灵活、可扩展"的关键。