文章目录
- 深入理解C++20无栈协程:从底层原理到Awaitable与co_yield
-
- 一、C++20无栈协程的核心底层模型
- 二、Awaitable类型:协程暂停/恢复的核心接口
-
- [2.1 Awaitable类型的设计初衷](#2.1 Awaitable类型的设计初衷)
- [2.2 Awaitable的核心规范](#2.2 Awaitable的核心规范)
- [2.3 Awaitable的两种实现方式](#2.3 Awaitable的两种实现方式)
-
- 方式1:自身作为等待器(最常用)
- [方式2:重载operator co_await生成等待器](#方式2:重载operator co_await生成等待器)
- [2.4 std::suspend_always/never的设计价值](#2.4 std::suspend_always/never的设计价值)
- 三、co_yield:Awaitable的语法糖
-
- [3.1 co_yield的底层本质](#3.1 co_yield的底层本质)
- [3.2 co_yield的设计价值](#3.2 co_yield的设计价值)
- 四、协程执行的完整生命周期(Generator示例)
- 五、核心总结
深入理解C++20无栈协程:从底层原理到Awaitable与co_yield
C++20引入的无栈协程是现代C++异步编程和生成器开发的核心特性,但其底层原理、Awaitable类型设计、co_yield语法糖的本质往往让初学者感到困惑。本文将从协程核心模型出发,由浅入深拆解协程的实现逻辑、Awaitable类型的设计初衷,以及std::suspend_always/never和co_yield的底层含义,帮助读者建立完整的协程知识体系。
一、C++20无栈协程的核心底层模型
要理解协程的各类语法和类型设计,首先需要掌握其底层核心原理:
C++20协程本质是无栈协程(Stackless Coroutine) ,与传统有栈协程不同,它不会为每个协程分配独立的调用栈,而是将协程的执行状态(局部变量、暂停点、指令指针)打包成一个堆上的状态对象。
协程的核心行为------暂停(suspend)与恢复(resume),本质是:
- 暂停:保存当前指令指针,释放调用栈,将协程状态保留在堆上;
- 恢复:恢复指令指针,重新占用调用栈,从上次暂停的位置继续执行。
为了管理这一过程,C++20协程设计了三个核心组件:
- 协程返回类型(如Generator):作为协程的"控制器",提供暂停/恢复接口;
- Promise类型:协程状态的载体,存储跨暂停点的数据,定义协程生命周期规则;
- Awaitable类型:决定协程是否暂停、暂停时的行为、恢复后的结果。
基础示例:Generator生成器
先通过一个完整的Generator示例建立直观认知,后续将围绕该示例拆解核心概念:
cpp
#include <coroutine>
#include <iostream>
#include <optional>
// 协程返回类型(控制器)
struct Generator {
using handle_type = std::coroutine_handle<>;
// Promise类型:协程状态载体
struct promise_type {
std::optional<int> value;
bool done = false;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
// 协程启动时暂停
std::suspend_always initial_suspend() {
std::cout << "[Promise] 协程启动,立即暂停\n";
return {};
}
// 协程结束时暂停
std::suspend_always final_suspend() noexcept {
done = true;
return {};
}
// 处理co_yield,保存值并暂停
std::suspend_always yield_value(int val) {
value = val;
std::cout << "[Promise] 捕获co_yield值:" << val << "\n";
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
explicit Generator(handle_type h) : coro_handle_(h) {}
~Generator() {
if (coro_handle_) 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;
coro_handle_.resume();
return !coro_handle_.done();
}
// 获取协程产出的值
int value() const {
return coro_handle_.promise().value.value();
}
private:
handle_type coro_handle_;
};
// 协程函数:生成1~3的序列
Generator generate_numbers() {
co_yield 1;
co_yield 2;
co_yield 3;
}
// 调用协程
int main() {
Generator gen = generate_numbers();
while (gen.resume()) {
std::cout << "[主线程] 获取值:" << gen.value() << "\n";
}
return 0;
}
二、Awaitable类型:协程暂停/恢复的核心接口
Awaitable(可等待对象)是C++20协程的灵魂,它定义了"协程何时暂停、暂停时做什么、恢复后返回什么"的规则。
2.1 Awaitable类型的设计初衷
C++20设计Awaitable的核心目标是通用化、可扩展的暂停/恢复逻辑。协程的暂停场景千差万别:
- 生成器需要"手动恢复"(如Generator);
- 异步任务需要"自动恢复"(如网络请求完成后);
- 条件执行需要"动态判断是否暂停"(如根据运行时参数)。
如果编译器硬编码暂停逻辑,无法适配所有场景。因此,C++20将暂停逻辑剥离到用户自定义的Awaitable类型中,编译器仅执行通用流程,具体逻辑由用户控制。
2.2 Awaitable的核心规范
任何能被co_await作用的类型,都称为Awaitable类型。其核心是提供一个符合规范的"等待器(awaiter)",该等待器必须实现三个成员函数:
| 函数 | 返回值 | 作用 |
|---|---|---|
await_ready() |
bool | 判断是否需要暂停:true=不暂停,false=需要暂停 |
await_suspend(h) |
void/bool/handle | 暂停时的逻辑:保存协程句柄、注册恢复回调;返回值控制是否真正暂停 |
await_resume() |
任意类型 | 恢复后的逻辑:返回等待结果(如异步任务的返回值、co_yield的产出值) |
co_await的编译展开逻辑
当执行co_await awaitable时,编译器自动展开为:
cpp
// 1. 获取等待器
auto&& awaiter = get_awaitable(awaitable);
// 2. 判断是否暂停
if (!awaiter.await_ready()) {
// 3. 暂停协程,执行暂停逻辑
awaiter.await_suspend(coroutine_handle);
}
// 4. 恢复后获取结果
auto result = awaiter.await_resume();
2.3 Awaitable的两种实现方式
方式1:自身作为等待器(最常用)
直接在类型中实现三个核心函数,该类型既是Awaitable,也是等待器。标准库的std::suspend_always/std::suspend_never就是典型示例:
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; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
方式2:重载operator co_await生成等待器
适用于为已有类型(如std::future)添加Awaitable能力:
cpp
template <typename T>
struct Future {
T value;
bool ready = false;
// 重载co_await运算符,返回等待器
auto operator co_await() {
struct Awaiter {
Future& future;
std::coroutine_handle<> handle;
bool await_ready() const noexcept { return future.ready; }
void await_suspend(std::coroutine_handle<> h) noexcept {
handle = h;
// 模拟异步完成后恢复协程
std::thread([this]() {
future.ready = true;
handle.resume();
}).detach();
}
T await_resume() noexcept { return future.value; }
};
return Awaiter{*this};
}
};
2.4 std::suspend_always/never的设计价值
这两个类型是Awaitable的"极简基础实现",解决了协程生命周期中最核心的默认需求:
| 场景 | 使用类型 | 作用 |
|---|---|---|
| 协程启动时暂停(Generator) | suspend_always | 协程创建后不立即执行,等待调用者手动resume(),避免一次性执行完毕 |
| 协程启动时立即执行(异步任务) | suspend_never | 协程创建后直接执行,直到遇到第一个co_await/co_yield才暂停 |
| 协程结束时暂停 | suspend_always | 避免协程结束后立即销毁堆状态,等待用户手动释放(防止内存泄漏) |
| co_yield时暂停 | suspend_always | 产出值后暂停协程,等待下一次resume()获取下一个值 |
在Generator示例中,initial_suspend()、final_suspend()、yield_value()都返回suspend_always,正是因为生成器需要"手动控制执行节奏"------调用者通过resume()逐次获取值,而非协程自动执行完毕。
三、co_yield:Awaitable的语法糖
co_yield是C++20为生成器场景设计的语法糖,其底层完全依赖Awaitable类型实现。
3.1 co_yield的底层本质
当编写co_yield val时,编译器自动将其展开为:
cpp
co_await promise.yield_value(val);
以Generator示例中的co_yield 1为例,完整执行流程:
- 调用
promise_type::yield_value(1),将值存入Promise(堆上); yield_value返回suspend_always(Awaitable类型);- 执行
co_await suspend_always:await_ready()返回false,需要暂停;await_suspend()无额外操作,协程暂停;- 等待调用者
resume()后,执行await_resume()(无返回值)。
3.2 co_yield的设计价值
- 简化代码 :生成器是协程的基础场景,
co_yield val比co_await promise.yield_value(val)更简洁; - 语义化:直接表达"产出一个值并暂停"的语义,贴合生成器的业务逻辑;
- 解耦性 :不依赖具体的Awaitable类型,只需
yield_value返回符合规范的Awaitable即可------既可以返回suspend_always(手动恢复),也可以返回自定义Awaitable(自动恢复)。
四、协程执行的完整生命周期(Generator示例)
结合以上知识点,梳理Generator的完整执行流程,理解各组件的协同工作:
-
创建协程:
- 调用
generate_numbers(),编译器分配堆上的协程状态对象(包含Promise、指令指针); - 调用
promise.get_return_object(),返回绑定协程句柄的Generator; - 调用
promise.initial_suspend(),返回suspend_always,协程在入口处暂停。
- 调用
-
第一次resume():
- 恢复协程,执行到
co_yield 1; - 调用
promise.yield_value(1),保存值并返回suspend_always; - 执行
co_await suspend_always,协程暂停; - 主线程通过
value()读取堆上的1。
- 恢复协程,执行到
-
后续resume():
- 重复步骤2,依次获取2、3;
- 第四次resume()时,协程执行完毕,调用
promise.final_suspend(),标记为done。
-
销毁协程:
- Generator析构时,调用
coro_handle_.destroy(),释放堆上的协程状态对象。
- Generator析构时,调用
五、核心总结
- 协程底层核心:无栈协程将执行状态存储在堆上,通过协程句柄控制暂停/恢复,Promise类型是状态载体;
- Awaitable设计初衷 :将暂停/恢复逻辑通用化、可扩展,通过
await_ready/await_suspend/await_resume三个接口定义核心规则; - std::suspend_always/never:极简的Awaitable实现,覆盖"无条件暂停/永不暂停"的基础场景,是协程生命周期管理的基础;
- co_yield本质 :
co_await promise.yield_value(val)的语法糖,专门简化生成器场景的暂停/值传递逻辑。
理解这些核心概念,就能掌握C++20协程的设计逻辑------编译器负责通用的生命周期管理,用户通过Promise和Awaitable定义具体的业务逻辑,实现灵活的暂停/恢复控制。