文章目录
- 深入理解C++20无栈协程:从底层原理到Awaitable与co_yield
- 一、C++20无栈协程的核心底层模型
- 基础示例:Generator生成器
- 二、Awaitable类型:协程暂停/恢复的核心接口
- 2.1 Awaitable类型的设计初衷
- 2.2 Awaitable的核心规范
- co_await的编译展开逻辑
- 2.3 Awaitable的两种实现方式
- 方式1:自身作为等待器(最常用)
- 方式2:重载operator co_await生成等待器
- 2.4 std::suspend_always/never的设计价值
- 三、co_yield:Awaitable的语法糖
- 3.1 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示例建立直观认知,后续将围绕该示例拆解核心概念:
#include<coroutine>#include<iostream>#include<optional>// 协程返回类型(控制器)structGenerator{usinghandle_type=std::coroutine_handle<>;// Promise类型:协程状态载体structpromise_type{std::optional<int>value;booldone=false;Generatorget_return_object(){returnGenerator{handle_type::from_promise(*this)};}// 协程启动时暂停std::suspend_alwaysinitial_suspend(){std::cout<<"[Promise] 协程启动,立即暂停\n";return{};}// 协程结束时暂停std::suspend_alwaysfinal_suspend()noexcept{done=true;return{};}// 处理co_yield,保存值并暂停std::suspend_alwaysyield_value(intval){value=val;std::cout<<"[Promise] 捕获co_yield值:"<<val<<"\n";return{};}voidreturn_void(){}voidunhandled_exception(){std::terminate();}};explicitGenerator(handle_type h):coro_handle_(h){}~Generator(){if(coro_handle_)coro_handle_.destroy();// 释放堆状态}// 禁用拷贝,启用移动Generator(constGenerator&)=delete;Generator&operator=(constGenerator&)=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;}// 恢复协程执行boolresume(){if(!coro_handle_||coro_handle_.done())returnfalse;coro_handle_.resume();return!coro_handle_.done();}// 获取协程产出的值intvalue()const{returncoro_handle_.promise().value.value();}private:handle_type coro_handle_;};// 协程函数:生成1~3的序列Generatorgenerate_numbers(){co_yield1;co_yield2;co_yield3;}// 调用协程intmain(){Generator gen=generate_numbers();while(gen.resume()){std::cout<<"[主线程] 获取值:"<<gen.value()<<"\n";}return0;}二、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时,编译器自动展开为:
// 1. 获取等待器auto&&awaiter=get_awaitable(awaitable);// 2. 判断是否暂停if(!awaiter.await_ready()){// 3. 暂停协程,执行暂停逻辑awaiter.await_suspend(coroutine_handle);}// 4. 恢复后获取结果autoresult=awaiter.await_resume();2.3 Awaitable的两种实现方式
方式1:自身作为等待器(最常用)
直接在类型中实现三个核心函数,该类型既是Awaitable,也是等待器。标准库的std::suspend_always/std::suspend_never就是典型示例:
// 无条件暂停structsuspend_always{constexprboolawait_ready()constnoexcept{returnfalse;}constexprvoidawait_suspend(std::coroutine_handle<>)constnoexcept{}constexprvoidawait_resume()constnoexcept{}};// 永不暂停structsuspend_never{constexprboolawait_ready()constnoexcept{returntrue;}constexprvoidawait_suspend(std::coroutine_handle<>)constnoexcept{}constexprvoidawait_resume()constnoexcept{}};方式2:重载operator co_await生成等待器
适用于为已有类型(如std::future)添加Awaitable能力:
template<typenameT>structFuture{T value;boolready=false;// 重载co_await运算符,返回等待器autooperatorco_await(){structAwaiter{Future&future;std::coroutine_handle<>handle;boolawait_ready()constnoexcept{returnfuture.ready;}voidawait_suspend(std::coroutine_handle<>h)noexcept{handle=h;// 模拟异步完成后恢复协程std::thread([this](){future.ready=true;handle.resume();}).detach();}Tawait_resume()noexcept{returnfuture.value;}};returnAwaiter{*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时,编译器自动将其展开为:
co_awaitpromise.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定义具体的业务逻辑,实现灵活的暂停/恢复控制。