协程 [ C++ ]

协程
C++20 [ 协程 ]
https://en.cppreference.com/w/cpp/language/coroutines.html
什么是协程?
打开 C++ 官方文档先看协程这个单词coroutine,拆解翻译过来就是协同程序,我们先搞懂协同程序的核心定义,再对比它和线程的区别。

首先我们先区分普通函数和协程。先说说我们日常写的普通函数是什么逻辑:当我们调用一个普通函数,函数内部完整执行完毕后,才会通过return返回给调用者。如果函数执行中途出现 I/O 等待这类阻塞操作,整个执行这个函数的线程都会被卡住,主线程无法处理其他任务,只能原地等待,直到阻塞完成、函数全部执行结束,才会回到调用处继续跑后续代码。整个普通函数生命周期只会完整返回一次,执行流程是单向、不可中断的。
而协程本质上也是一种函数,只是是 C++20 新增的特殊函数。C++20 专门定义了三个协程专属关键字:co_await、co_yield、co_return,只要函数内部包含这三个关键字任意一个,它就属于协程。
协程最核心的特性是执行中途可以挂起暂停:调用协程后,代码正常向下执行,一旦碰到co_await/co_yield/co_return,协程就会主动挂起。挂起时会完整保存当前运行状态:程序计数器(记录执行到哪一行指令)、所有局部变量、少量寄存器,还有协程专属的控制对象等。保存完状态后,协程让出执行权,调用方可以继续执行其他业务逻辑;等到满足唤醒条件后,再恢复协程,从上次挂起的位置接着往下执行。

一个协程生命周期内可以多次挂起、多次恢复,反复切换执行,直到内部所有逻辑全部跑完,执行co_return后,协程资源才会销毁。
有人可能会疑惑:普通函数写起来简单,为什么还要新增协程这个复杂语法?协程核心作用是实现低成本异步 IO 逻辑,最典型的使用场景就是高并发网络服务器。举个网络服务的例子:服务器接收到客户端网络连接后,需要调用read接口读取客户端发送的数据。如果客户端数据还没传输完成,普通阻塞式read会直接卡住当前线程。
所以简单来说,协程是一个可以暂停执行并在之后恢复执行的函数。
-
普通函数:调用 -> 执行到
return-> 销毁所有栈帧,返回调用者。一生只返回一次。 -
协程:调用 -> 执行到
co_await,co_yield,co_return-> 暂停(挂起),保存当前状态(局部变量、执行位置等)-> 之后某个时刻被恢复,从暂停点继续执行 -> 最终执行到co_return结束。
协程和线程的关系
传统高并发服务会使用多线程处理这类 IO 阻塞,但线程切换存在很大短板:线程切换属于内核态上下文切换,操作系统需要保存全部 CPU 寄存器、更新页表,资源开销巨大。同时系统能创建的线程总数存在硬性上限,每个线程都要分配独立栈空间,如果服务承载几百万客户端连接,不可能对应创建几百万个线程,大量线程之间频繁抢占 CPU、来回切换,会带来极高的性能损耗。
而协程完美解决了这个痛点:同一个线程可以承载大量协程。线程执行某个协程时,一旦协程遇到 IO 等待需要阻塞,不会阻塞整个线程,仅把当前协程挂起保存状态,线程立刻调度其他就绪的协程继续执行;等之前协程等待的 IO 数据到达后,再把它恢复回来接着运行。
协程切换全程只在用户态完成,不会陷入内核,仅需要保存少量寄存器、局部变量、协程帧、Promise对象、协程句柄等少量数据,上下文切换成本极低,能支撑海量并发 IO 任务。
接下来我们区分协程和线程的关系,很多人会混淆二者,误以为协程是用来替代线程的,这里纠正核心概念:协程和线程是两个不同维度的技术,二者是协作互补关系,不存在互相替代的说法。
我们用工厂比喻方便大家理解:操作系统相当于工厂,CPU 核心是流水线,线程就是流水线上干活的工人,操作系统内核调度器负责分配工人(线程)到不同 CPU 流水线执行,线程是操作系统调度任务的最小执行单元。
协程是工人手里一套可以随时暂停、切换的任务模具。同一个工人(线程)不用等一个任务完全做完,当手里的任务需要等待 IO、无法继续推进时,可以把这个任务模具(协程)暂停收好,立刻拿起另一套任务模具(其他协程)继续干活;等之前任务等待的资源就绪,再换回这套模具,从上次停下的位置继续完成。
这就意味着:单个线程可以同时运行成千上万个协程。传统多线程方案里,线程一旦执行阻塞 IO,整个线程就会被操作系统切走,CPU 时间片被浪费;但搭配协程后,线程不会因为单个 IO 任务阻塞而闲置,全程可以持续调度其他就绪协程,CPU 利用率大幅提升。
工业界主流最佳实践是协程 + 线程池组合使用:线程池提供多线程并行计算能力,协程负责在线程内部高效管理海量并发 IO 任务,二者结合兼顾并行计算与高并发 IO 处理。
二者切换成本的核心差距:
线程切换:需要陷入内核态,保存全部通用寄存器、更新页表、修改内核调度队列,资源开销大,频繁切换会严重损耗性能;
C++20 协程切换:纯用户态操作,仅保存少量寄存器、局部变量、协程帧、Promise、协程句柄等轻量数据,挂起 / 恢复成本极低。
所以总结来说:
-
C++20协程和线程是不同维度的概念,它们之间是协作与互补的关系,而不是替代关系,线程是劳动力(CPU核心),协程是高效的任务(Task)。 -
线程:像是工厂里的工人。操作系统是厂长,负责分配工人(线程)到不同的流水线(
CPU核心)上工作。工人是执行任务的基本单位。 -
协程:像是工人手中的一套可暂停、可换模的任务,一个工人可以在一个任务做到一半时暂停(挂起协程),换上另一套模具做另一个任务(恢复另一个协程),之后再回来继续第一个任务。
-
协程不是线程的替代品,而是增强工具,最佳实践是将协程与线程池结合使用,用线程池提供并行计算能力,用协程来高效管理大量的并发
I/O任务。 -
线程切换的代价很高,需要上下文切换,陷入内核态,保存所有寄存器、更新页表等。而
C++20协程的上下文切换代价极低,纯粹在用户态进行,只需保存 / 恢复少量寄存器。
C++20 协程
C++20 协程:编译器原生无栈协程
有栈协程 vs 无栈协程
协程整体分为两大实现路线:有栈协程、无栈协程,C++20 标准内置的协程属于无栈协程,二者可以用一个生活化比喻区分:
-
有栈协程:每一个协程都分配独立栈空间,相当于每个协程单独拥有一张办公桌。协程挂起切换时,需要完整保存当前整张栈的所有数据,切换后再切换另一张独立栈运行。 市面上第三方库实现的协程大多是有栈协程,比如 Boost 库自带协程、腾讯早年自研的内部协程库都属于这类。这类库无法依托 C++ 语法层实现无栈逻辑,只能在用户态封装,底层做不到轻量化。
-
无栈协程:整个线程只共用一套基础栈,没有为每个协程单独分配栈空间。协程挂起时,不会保存完整调用栈,仅把运行必需的少量数据(程序计数器、局部变量、Promise 对象等)统一存放到堆上开辟的协程帧中;再次调度其他协程时,只读取、替换堆上存储的少量状态信息即可。
两种实现各有优劣,但无栈协程综合优势更强:
-
内存占用极低:单个协程仅占用少量堆内存,系统可以创建数十万、上百万个协程,不会受线程 / 独立栈内存上限限制;
-
切换开销极小:切换仅读写堆上少量状态数据,全程用户态完成,不会陷入内核;
无栈协程唯一明显短板:协程内部不能执行阻塞操作。协程依附于线程运行,如果协程内部调用阻塞接口,整个承载它的线程都会被卡住,所有挂载在线程上的协程全部停止调度,这是使用无栈协程必须规避的问题。
无栈协程只能依托编译器底层实现,第三方库单纯在应用层封装很难完成,这也是 C++20 协程轻量化的核心原因。现阶段大家不用深究底层实现细节,只需要记住核心结论:C++20 是编译器原生无栈协程,轻量、切换成本低,但协程内部禁止阻塞。
C++20 协程是无栈协程,没有独立运行栈,挂起时仅在堆上分配协程帧,保存 promise、暂停位置、局部变量,不需要保存完整调用栈,所以非常轻量;同时无栈特性决定:同一调用链内,子协程一旦挂起,上层调用方没有独立运行栈保存现场,整条调用链路都会暂停,无法分层独立调度。 协程的挂起、恢复、切换逻辑全部运行在用户态,不需要切换内核上下文,开销远小于线程;但要区分:如果协程是等待 IO 而挂起,awaiter 内部会通过系统调用和内核 epoll 交互,后续事件循环调用 epoll_wait 时,底层线程会进入内核阻塞休眠,这是线程层面的内核行为,和协程自身用户态切换是两回事。
落到业务应用选型上两种架构有明确定位区分: 主线程 epoll 监听 + 线程池执行,适配存在大量 CPU 密集计算、同步阻塞数据库、复杂报文编解码等长耗时业务。IO 就绪事件统一分发到线程池多线程并行处理,利用多核算力分担重任务,避免耗时逻辑阻塞 IO 监听循环,保证海量连接的 IO 事件能被及时响应;代价是多线程存在共享资源锁竞争、线程切换带来内核开销。注意:线程池内如果存在同步阻塞 IO,会持续占用工作线程,高并发场景容易耗尽池内线程。
单 / 多 Reactor 线程内部搭配 C++20 协程,适配网络转发、轻量路由、简单报文处理等短耗时、纯 IO 密集型任务。单线程内依靠协程用户态低开销切换处理上万连接,全程无锁、调度延迟极低;但一旦混入重度耗时逻辑,会直接阻塞整个 Reactor 事件循环,造成 IO 事件堆积、请求延迟飙升。
C++20 协程它本身不是一个开箱即用的并发工具,而是一个用于构建异步编程的强大底层语言设施,项目要使用协程,通常要使用一些上层封装抽象的协程库或者结合使用的库,如 libunifex/cppcoro/Boost.Asio 等等。
所以:协程核心价值依托异步 IO 才能最大化,但 C++ 协程脱离异步也有独特同步场景用途;只是纯 CPU 计算场景收益极低。
即便不搭配事件循环、不做网络 IO,C++ 协程依然能解决普通函数 / 类难以实现的需求,属于协程独有能力:
1. 分段迭代、流式生成数据(替代手写迭代器)
普通函数一次性返回全部数据,内存占用高;协程通过co_yield分段产出,边生成边消费,节省内存。
generator<int> range(int n) {
for (int i = 0; i < n; ++i) {
co_yield i; // 暂停,返回当前值,下次从这里继续执行
}
}
// 使用
for (auto num : range(1000000)) {
process(num);
}
适用场景:超大文件逐行解析、数据流处理、数值遍历。
2. 轻量化状态机,消除大量状态变量
协议解析、游戏 AI 逻辑、分段任务,不用手动维护state枚举变量,代码线性书写,可读性大幅提升。 普通写法:多层 if-else、大量状态标记; 协程写法:按执行顺序写逻辑,co_await暂停等待外部信号,天然保存执行现场。
3. 协作式分时多任务(单线程无锁并发)
嵌入式、无多线程环境、游戏主线程:单线程内调度多个协程轮流执行,每帧主动让出执行权,避免单任务阻塞主线程。 典型场景:游戏多角色 AI、UI 逻辑、嵌入式设备多任务调度。
4. 可中断、可恢复业务流程
复杂分步业务(订单流程、批量任务),执行到一半可暂停、保存上下文,后续恢复继续运行,不用手动序列化中间变量。
-
IO 密集异步场景(网络 / RPC / 数据库) 价值:极高,工业级高并发服务首选,线程无法替代;
-
纯 CPU 密集计算(无等待、无 IO) 价值:极低。协程无法利用多核并行,纯计算不如
std::thread/ 多进程;仅当需要分段产出、中途暂停时才有用; -
简单线性业务逻辑 价值:无。普通函数性能更高,引入 C++ 协程会增加模板、awaitable 语法复杂度,得不偿失;
-
流式迭代 / 状态机 / 游戏帧逻辑 价值:中等,同步场景下协程是最优写法。
C++20 协程学习曲线陡峭,且当前直接使用很麻烦困难,但是学习它对于我们使用和理解协程的运行机制和高效使用协程库有很大的帮助,当前C++23提供了 std::generator 相关支持,C++26 也在进一步支持task等,C++26当前还没正式发布,我们需要静等后续C++标准库的完善和编译器的支持。
简单来说,当一个函数包含 co_await/co_yield/co_return 任意一个关键字时,它就是协程。
-
co_await expression用于暂停执行直到恢复 -
co_yield expression用于暂停执行并返回一个值 -
co_return expression用于完成执行并返回一个值
编译器会将这个函数转换为一个状态机,关键在于,协程的行为由其返回类型(称为Promise类型)控制。
// C++23支持的generator
#include <generator>
#include <iostream>
std::generator<long long> fibonacci(int count) {
long long a = 0, b = 1;
for (int i = 0; i < count; ++i) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
int main() {
for (auto num : fibonacci(10)) {
std::cout << num << " "; // Output: 0 1 1 2 3 5 8 13 21 34
}
}
协程的控制三个核心组件
只要函数内部包含co_await、co_yield、co_return三个关键字任意一个,这个函数就属于协程函数:
-
co_await:核心关键字,用于主动暂停协程,等待异步操作完成后恢复; -
co_yield:暂停协程,同时向外产出一个返回值,典型用于生成器; -
co_return:终止协程生命周期,返回最终结果;
编译器遇到协程函数时,会自动把函数逻辑转换成状态机,通过状态机记录协程挂起、恢复的执行位置。协程整体运行行为,完全由它的返回类型控制,这个返回类型我们统一称作Promise类型。
想要完整实现一个可运行的协程,不能只写带关键字的函数,必须配套实现三大核心控制组件,整套代码繁琐、模板代码极多,直接看纯文字课件很难理解,后面我们会结合完整生成器样例逐行拆解,这里先简单介绍三大组件基础概念。
下面这些,我们可以结合再下面的案例学习,知道为什么需要提供相关的核心接口!
Promise 对象
由编译器在协程帧内创建的对象,它是协程的大脑,控制着协程的行为,Promise 类型必须提供以下核心接口,用来控制协程的行为。
1. get_return_object()
a. 作用:创建并返回给调用者协程的返回值。这是在协程函数体执行之前被调用的。
b. 返回值类型:协程的返回类型(例如外层的 Task、Generator)。 典型实现:通常会用 coroutine_handle<Promise>::from_promise(*this) 来创建一个指向当前Promise对象的句柄,并用这个句柄来构造外层的返回值对象。
Generator get_return_object() {
return Generator{std::coroutine_handle<Promise>::from_promise(*this)};
}
2. initial_suspend()
a. 作用:决定协程在开始执行函数体之前是否应该先挂起。
b. 返回值:一个满足 Awaitable 概念的对象(通常是 std::suspend_always 或 std::suspend_never)。
c. std::suspend_always{}:总是挂起。这意味着协程一创建就是惰性的,不会自动开始执行,需要手动 resume(),这是 “懒汉式” 协程的常见选择。
d. std::suspend_never{}:从不挂起。协程创建后会立即开始执行,直到遇到第一个挂起点。这是 “俄式” 协程的常见选择。
3. final_suspend()
a. 作用:决定协程在执行完函数体(或通过 co_return 返回)后,在彻底销毁自身之前是否应该挂起。
b. 返回值:同样是一个 Awaitable 概念对象。(通常是 std::suspend_always 或 std::suspend_never)。
c. 重要提示:如果返回 std::suspend_always,你必须在协程外部某处手动调用 .destroy() 来销毁协程帧,否则会发生内存泄漏。如果返回 std::suspend_never,协程会在返回后自动清理自己。
d. 典型用途:使用返回 std::suspend_always,在 final_suspend() 中挂起,可以让你在协程外部有机会在其销毁前读取最终状态或结果。
4. return_void() 和 return_value(...)
a. 作用:处理 co_return 语句。
b. Promise 类型必须实现这两个的其中之一,但不能同时实现。
c. void return_void():用于处理 co_return;(无返回值)。
d. void return_value(type value):用于处理 co_return value;(有返回值)。 value 的类型由你指定,通常你会把它存储到 Promise 的一个成员变量中,以便外部获取。
5. yield_value(...)
a. 作用:处理 co_yield 语句。co_yield expression 本质上被编译器翻译为 co_await promise.yield_value(expression)。
b. 返回值:一个 Awaitable 概念对象,用于决定在产出值之后是否要挂起协程(通常返回 std::suspend_always{} 来挂起,让调用者有机会处理产出的值)。
c. 典型实现:接收产出的值,将其存储到 Promise 类型的一个成员变量中,然后返回一个挂起指示器。
std::suspend_always yield_value(int value) {
current_value = value; // 存储值到current_value成员变量,供外部访问
return {};
}
6. unhandled_exception()
a. 作用:当协程体内发生异常,并且该异常未被协程体内的 try/catch 块捕获时,这个异常会被自动传递给 unhandled_exception() 方法。
b. unhandled_exception():Promise 类型的核心异常处理函数,协程内任何未捕获的异常都会调用此函数,std::current_exception():捕获当前异常并转换为 std::exception_ptr,std::rethrow_exception():重新抛出存储的异常。
c. RAII 模式在协程中依然有效,即使协程抛出异常,栈上的对象也会正确析构,协程帧本身的内存由 coroutine_handle 管理
#include <coroutine>
#include <iostream>
#include <stdexcept>
// 最简单的协程类型
struct SimpleTask {
struct promise_type {
int value = 0;
std::exception_ptr exception; // 存储异常的地方
SimpleTask get_return_object() {
return SimpleTask{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
// 关键:异常处理函数
void unhandled_exception() {
std::cout << "捕获到异常,存储起来\n";
exception = std::current_exception(); // 捕获异常
}
void return_value(int val) {
value = val;
}
};
std::coroutine_handle<promise_type> handle;
// 获取结果,如果有异常就抛出
int get_result() {
if (!handle.done()) {
handle.resume();
}
if (handle.promise().exception) {
std::cout << "重新抛出存储的异常\n";
std::rethrow_exception(handle.promise().exception);
}
return handle.promise().value;
}
~SimpleTask() {
if (handle) handle.destroy();
}
};
// 会抛出异常的协程函数
SimpleTask throwing_coroutine() {
std::cout << "异常协程开始执行\n";
std::cout << "准备抛出异常\n";
throw std::runtime_error("协程内部发生错误!"); // 抛出异常
co_return -1; // 这行不会执行
}
// 立即抛出异常的协程
SimpleTask immediate_throw_coroutine() {
std::cout << "立即抛出异常的协程\n";
throw std::logic_error("立即发生的错误!");
co_return 100; // 不会执行
}
int main() {
std::cout << "=== 测试异常协程 ===\n";
try {
auto task2 = throwing_coroutine();
int result = task2.get_result(); // 这里会抛出异常
std::cout << "结果: " << result << "\n\n";
} catch (const std::runtime_error& e) {
std::cout << "捕获到 runtime_error: " << e.what() << "\n\n";
}
std::cout << "=== 测试立即异常 ===\n";
try {
auto task3 = immediate_throw_coroutine();
int result = task3.get_result(); // 这里会抛出异常
std::cout << "task3: " << result << "\n\n";
} catch (const std::logic_error& e) {
std::cout << "捕获到 logic_error: " << e.what() << "\n\n";
}
return 0;
}
7. await_transform()
a. await_transform 是 Promise 类型的一个可选成员函数,它允许在 co_await 表达式上应用自定义转换。当编译器遇到 co_await expression 时,会查找 Promise 类型的 await_transform(type expression) 方法,找不到就编译报错,本质上就是支持把一个 expression 变成 Awaitable 对象。
b. 返回值:一个 Awaitable 概念对象。
struct FileReadAwaiter {
int fd;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 启动异步IO,例如调用 io_uring / libuv 来异步读取 fd
// 当读取完成时,恢复协程 h
// async_read(fd, h);
}
std::string await_resume() {
// 返回读取到的数据
return read_buffer;
}
};
struct MyPromise {
// ... 其他 Promise 成员函数同上
// 当协程内 co_await 一个 int 时,我们将其视为文件描述符
auto await_transform(int fd) {
std::cout << "Interpreting int as file descriptor for async read\n";
return FileReadAwaiter{fd};
}
};
MyCoroutine my_coroutine() {
int socket_fd = open_socket(); // 假设返回一个文件描述符
std::string data = co_await socket_fd; // 神奇地变成了异步读取!
std::cout << "Received: " << data << std::endl;
}
协程句柄
协程句柄是一个非拥有的、轻量级的句柄,它代表并允许你从外部直接操作一个特定的协程帧,它是你与协程运行时交互的主要接口,你可以把它想象成一个指向特定协程的遥控器,有点类似于系统 API 的线程句柄,文件句柄,或者理解成类似指向 Promise 对象的智能指针,但是功能跟智能指针就不搭边了,智能指针的核心是要控制资源的释放,这里是要管理 Promise 对象。
通常我们在协程返回类型中定义 std::coroutine_handle<Promise> handle 的句柄成员变量。
协程句柄的核心操作 API 主要有以下几个,更多的 API 请看文档
协程句柄
https://en.cppreference.com/w/cpp/coroutine/coroutine_handle.html
1. from_promise() / promise()
a. static coroutine_handle from_promise(Promise& p):用于使用一个 Promise 对象创建一个指向 Promise 对象的句柄,通常在 Promise 类型的 get_return_object 成员函数中使用。
b. Promise& promise() const:方便我们使用句柄可以获取管理的 Promise 对象。
2. resume() / operator()
a. 作用:让一个处于挂起状态的协程继续执行。
b. 行为:调用后,协程从上次挂起的地方开始执行,直到遇到下一个挂起点、结束或抛出异常。
c. 注意:不能在非挂起状态的协程上调用(如已运行完成或尚未开始的协程,行为未定义)
3. done()
a. 作用:查询协程是否已执行完成(即是否已经到达函数体末尾或执行了 co_return)。
b. 返回值:true:协程已执行完成,false:协程仍在运行或处于挂起状态。
4. operator bool()
a. 作用:查询是否是一个有效的协程句柄。
b. 返回值:true:指向一个有效的协程,false:协程句柄内存指向一个 nullptr 指针。
5. destroy()
a. 作用:显式销毁协程帧,释放其内存。
b. 何时调用:通常在协程返回类型的析构中调用,只有当协程在 final_suspend() 返回 std::suspend_always 挂起时,你才需要(且必须)手动调用 .destroy()。如果 final_suspend() 返回 std::suspend_never,协程会自动销毁自己,你再调用 .destroy() 就是未定义行为。
c. 重要:忘记调用 .destroy() 会导致内存泄漏。
6. operator coroutine_handle<>()
a. 作用:该转换函数将 std::coroutine_handle<Promise> 值转换为包含相同底层 address 的 std::coroutine_handle<> 值。
b. 何时调用:void await_suspend(std::coroutine_handle<> h) 中作为形参接收各协程的句柄。
Awaitable 对象
在C++20协程中,任何可以出现在 co_await 表达式右边的对象,都是一个 Awaitable 对象。而且三个关键字里co_await使用场景最多,co_yield底层会被编译器自动转换为co_await,co_return仅在协程收尾时使用,所以Awaitable是三大组件里最核心、最复杂的部分。
co_await关键字右侧必须跟一个Awaitable类型对象【定义当协程执行到 co_await expression 的行为】,自定义Awaitable类型需要强制实现三个成员函数,分别管控协程暂停全流程:
-
await_ready():判断当前是否需要暂停协程,返回true代表无需挂起,直接执行后续逻辑;返回false代表需要挂起协程; -
await_suspend():协程确认要挂起、但还未暂停的瞬间执行,核心用途是注册异步回调。网络编程中最常用:注册 IO 多路复用事件,等 IO 读写事件就绪后,通过句柄恢复协程; -
await_resume():协程被唤醒恢复执行后调用,用来获取异步操作的最终结果,也可以在这里抛出异常传递给协程。
简单总结:Awaitable完整管控协程要不要暂停、暂停前做什么、恢复后怎么拿结果,是协程异步调度的核心载体。
一个类型要成为 Awaitable,它必须实现三个特定的成员函数 await_ready/await_suspend/await_resume,或者通过 operator co_await 重载返回一个实现了这些函数的对象
编译器会将 co_await expression 转换为一系列对这些函数的调用,让我们分解这个过程。
{
auto&& awaitable = <expression>; // 1. 获取 awaitable 对象
if (!awaitable.await_ready()) { // 2. 检查是否 ready
// 3. 准备暂停
// 4. 调用 await_suspend,传入当前协程的句柄
// 5. 根据 await_suspend 的返回值决定行为(见下文)
// ...
<suspend coroutine> // 协程在此处暂停
// ... 异步操作在后台进行 ...
// ... 当异步操作完成时,通过某种方式恢复协程 ...
// 6. 协程在此时恢复执行
}
// 7. 获取结果
return awaitable.await_resume(); // 整个 co_await 表达式的结果
}
1. bool await_ready() const
a. 目的:性能优化。在尝试暂停之前,检查异步操作是否已经完成。
b. 返回值:true:表示结果已就绪,无需暂停协程。编译器将跳过 await_suspend 和暂停步骤,直接调用 await_resume 并继续执行。false:表示结果未就绪,需要暂停协程,继续执行 await_suspend 逻辑。
c. 最佳实践通常实现为 noexcept。对于立即完成的操作(如缓存命中),返回 true 可以避免不必要的暂停开销。
2. type await_suspend( std::coroutine_handle<> ) const
a. 这是最强大也是最复杂的部分,它在协程即将暂停但尚未暂停时被调用。
b. 目的:安排异步操作的回调,它的职责是获取当前协程的句柄 handle,并将其传递给某个异步操作,以便在操作完成后恢复协程。
c. 参数:std::coroutine_handle<>,代表当前正在执行的协程,你可以保存它、传递
d. 返回值和行为:返回值类型至关重要,它决定了控制流的走向
e. 返回值行为为 void,默认行为,协程暂停,控制权返回到当前协程调用者。这是最常见的情况。
f. 返回类型为 bool,true 表示协程已挂起到后台(可能在另一个线程恢复);false 表示不挂起,立即恢复,用于在最后关头发现需要等待的操作已完成的情况。
g. 返回类型为 std::coroutine_handle<>,另一个协程句柄 handle 协程暂停,然后立即恢复 handle 所代表的协程,这实现了对称转移,是无栈协程链式调用的关键。
h. 最佳实践:await_suspend 通常实现为 noexcept,在这里你通常会调用某个异步 API,并将 handle 作为其完成回调,绝对不要在此函数内阻塞。
struct promise_type {
std::coroutine_handle<> handle;
// ...
};
struct Awaiter {
bool await_ready() noexcept { return false; }
// 这里就是返回另一个协程句柄的地方!
template<typename Promise>
std::coroutine_handle<> await_suspend(
std::coroutine_handle<Promise> current) noexcept {
// 获取当前协程的promise
auto& promise = current.promise();
// 返回另一个协程的句柄(如果存在)
if (promise.handle) {
return promise.handle; // 对称转移到另一个协程
} else {
// 没有父协程,返回空句柄(回到主线程)
return std::noop_coroutine();
}
}
void await_resume() noexcept {}
};
3. type await_resume() const
a. 目的:获取结果或处理错误。在协程恢复后或 await_ready 返回 true 时,此函数被调用以产生 co_await 表达式的结果。
b. 返回值:可以是 void 或任何其他类型,这个返回值就是整个 co_await 表达式的结果。
c. 如果异步操作产生一个值,await_resume() 返回这个值。
d. 如果异步操作可能失败,await_resume() 可以检查错误码并选择返回一个值或抛出一个异常。
e. 最佳实践:可以抛出异常,不抛异常时标记为 noexcept。
C++20 在标准库中提供了两个最简单的 Awaitable 类型
std::suspend_always
-
await_ready():返回false,总是暂停。 -
await_suspend(...):无操作,直接返回void。 -
await_resume():返回void。 -
用途:用于
initial_suspend()和final_suspend(),表示请在此处暂停
std::suspend_never
-
await_ready():返回true,从不暂停。 -
await_suspend(...):永远不会被调用。 -
await_resume():返回void。 -
用途:用于
initial_suspend(),表示请不要暂停,立即开始执行协程体。
operator co_await() operator co_await() 是一个用户定义的转换函数,它允许将自定义类型转换为可以被 co_await 使用的 Awaitable 类型。
#include <coroutine>
class AdvancedTask {
int value_;
public:
AdvancedTask(int value) : value_(value) {}
// 对于 const 左值的 operator co_await
auto operator co_await() const & noexcept {
struct ConstLvalueAwaiter {
const AdvancedTask& task_;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<>) const {}
int await_resume() const {
return task_.value_ * 2; // 某种转换
}
};
return ConstLvalueAwaiter{*this};
}
// 对于右值的 operator co_await
auto operator co_await() && {
struct RvalueAwaiter {
AdvancedTask&& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
int result = task_.value_;
task_.value_ = -1; // 可以修改,因为是右值
return result;
}
};
return RvalueAwaiter{std::move(*this)};
}
// 对于非const左值的 operator co_await
auto operator co_await() & {
struct LvalueAwaiter {
AdvancedTask& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
task_.value_ += 10; // 可以修改
return task_.value_;
}
};
return LvalueAwaiter{*this};
}
};
co_await AdvancedTask(10);
协程学习样例之生成器
之前我们讲过,C++20 在语言层面只提供协程底层基础框架与底层工具支撑,并不是封装好可以直接拿来调用的成品工具。这也就意味着,协程完整的调度、控制逻辑,需要我们自己手动实现三大核心组件来完成。
今天我们先看第一个入门样例 —— 生成器。这个样例比较简单,不需要完整用到三大组件,只需要吃透前两个核心组件:Promise 对象、协程句柄,门槛更低,适合用来入门理解底层运行逻辑。
单纯干讲 Promise 和协程句柄很难记牢,所以我们结合这份经典极简生成器代码样例,反向拆解里面的核心知识点,最后再统一做总结梳理。这里如果直接空讲理论,大家很难听懂,所以我们先看懂代码运行逻辑,再反向推导底层机制。
以下样例主要实现一个 co_yield 的协程,主要通过 Promise 对象和协程句柄控制协程,几乎是最简单的协程程序。
#include <coroutine>
#include <exception>
#include <iostream>
// 1. 协程的返回类型
struct Generator {
// 2. 核心:promise_type
struct promise_type {
int current_value; // 用于存储产出的值
// 2a. 创建返回对象
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 2b. 初始挂起:选择挂起,让协程惰性执行
std::suspend_always initial_suspend() { return {}; }
// 2c. 最终挂起:选择挂起,我们需要手动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2d. 处理 co_yield
std::suspend_always yield_value(int value) {
current_value = value;
return {}; // 产出后总是挂起
}
// 2e. 处理 co_return; (无值返回)
void return_void() {}
// 2f. 处理异常
void unhandled_exception() { std::terminate(); }
};
// 3. Generator 类本身的成员
// 协程句柄,用于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数和析构函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() {
if (handle) handle.destroy(); // 负责销毁协程帧
}
// 4. 提供给外部的API
// 获取当前值
int value() const {
return handle.promise().current_value;
}
// 恢复执行直到下一个co_yield或结束
bool next() {
if (!handle.done()) {
handle.resume();
}
return !handle.done();
}
};
// 使用这个生成器的协程函数
Generator range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // 这会调用 promise.yield_value(i)
}
// 协程结束,调用 promise.return_void()
}
int main() {
auto gen = range(1, 5); // 创建协程,initial_suspend挂起,此时协程未执行
while (gen.next()) { // 恢复协程,协程执行到co_yield处挂起
std::cout << gen.value() << " "; // 从promise中获取产出的值
}
// 输出: 1 2 3 4
}
整体框架介绍
先区分普通函数和协程函数的执行差异:只要函数内部包含 co_yield、co_await、co_return 任意一个关键字,它就是协程函数,具备中途暂停、后续恢复的特性,我们这份生成器样例里用到了 co_yield,所以它是标准协程函数。
传统普通函数的执行流程很直白:调用函数 → 函数从头到尾完整执行完毕 → return 回到调用处,中间无法中断。但协程完全不一样:当我们调用生成器协程函数时,函数不会一次性全部跑完,执行到指定位置就会直接挂起暂停。我们可以结合调试断点直观验证: 在协程内部的 for 循环第一行打上断点,单步调试按 F10 向下走,程序不会停在这个断点,说明协程调用完成后,直接在循环执行前就挂起了,循环逻辑根本没有开始运行。
那什么时候协程才会继续往下执行?需要我们在外部调用 next() 方法手动唤醒恢复协程。 整体完整运行流程拆解:
-
外部调用协程函数,创建协程对象,协程立刻挂起,直接回到主调用逻辑,内部循环不会执行;
-
外部调用
gen.next(),唤醒挂起的协程,协程从上次暂停的位置继续执行; -
代码运行到
co_yield关键字时,会把产出的值存入Promise对象内部的变量,随后协程再次挂起,把执行权交还给外部主逻辑; -
外部调用
gen.value(),就能从Promise中取出刚刚产出的值,完成打印; -
循环重复执行:调用
next()恢复 → 执行到co_yield产出值并挂起 → 读取值打印,协程在「挂起 - 恢复」之间来回切换,直到循环全部执行完毕。
这也是为什么把它叫做生成器:它不会一次性把所有值全部计算出来,而是按需执行、分次产出数据,依靠协程可暂停、可恢复的特性实现按需生成。
这段代码如果不拆解底层机制,只看表面会有很多疑惑:
-
协程函数返回类型写的是
Generator,函数内部却没有写return Generator{},但外部能直接接收这个对象; -
next()、value()这些成员函数是怎么和协程内部逻辑联动的; -
协程创建、挂起、恢复的底层流程完全看不到。
如果搞不懂底层三大组件的作用,代码哪怕能正常运行,你也完全说不清楚内部执行原理,遇到报错、想要自定义逻辑时根本无从下手,这也是协程难学的核心原因。
接下来我们结合这份生成器代码,逐段拆解两大核心组件:Promise 类型、协程句柄。
想要自定义一个合法的 C++20 协程,必须手动定义 Promise 相关类型,它是协程的核心控制单元,相当于协程的大脑。我们需要在这个类型内部,按照标准规定实现固定名称、固定参数、固定返回值的成员函数,编译器会在协程生命周期的不同阶段自动调用这些函数。
行业内约定将这个类型命名为 promise_type,当然命名没有强制语法限制,只是为了可读性,大家看到这个名字就能立刻识别出这是协程的控制类。这个类型通常作为协程返回类型(也就是示例中的 Generator)的内部嵌套类,也可以定义为外部类,嵌套类是更常用的写法。
协程创建时,编译器会在堆上分配一块内存,叫做协程帧,Promise 对象会直接创建存储在这块协程帧内存中。协程每次挂起时,执行位置(程序计数器)、局部变量、寄存器状态、Promise 对象都会保存在协程帧里,等待后续恢复执行。
生成器需要临时存储每次 co_yield 产出的数据,所以我们会在 promise_type 内部定义一个成员变量,比如示例里的 int current_value;如果生成字符串就定义 std::string,批量生成数据也可以用容器,这个成员变量是我们按需自定义的,没有标准强制要求。
标准强制规定了一套固定函数,函数名、参数、返回类型都不能随意修改,缺少任意一个都会直接编译报错,这些函数会由编译器在协程不同生命周期自动调用,类似对象创建时自动调用构造函数,是协程运行的底层回调:
-
get_return_object():协程创建初期自动调用,用来创建并返回协程对外暴露的返回对象(示例中的Generator); -
initial_suspend():控制协程创建完成后是否立刻挂起,也就是初始挂起逻辑;单词suspend本身就是暂停、挂起的含义; -
final_suspend():协程全部逻辑执行完毕、即将销毁前的最终挂起控制; -
return_void():处理无返回值的co_return;语句,协程正常结束时触发; -
yield_value(T value):专门处理co_yield关键字,co_yield 数值底层会自动调用这个函数,把产出值存入Promise; -
unhandled_exception():捕获协程内部没有被try/catch捕获的异常,统一做异常处理。
示例代码里我们对这六个接口都做了最简实现,满足标准语法要求。
光有 Promise 类型还不够,我们需要第二个组件 —— 协程句柄,用来从外部访问、操控存放在协程帧里的 Promise 对象。
std::coroutine_handle 是 C++ 标准库内置的类模板,模板参数就是我们自定义的 promise_type。我们可以把协程句柄理解成一个特殊指针,内部存储了协程帧中 Promise 对象的地址,功能上和智能指针有一点点相似,但二者核心职责完全不同:
-
智能指针核心作用是自动管理内存释放;
-
协程句柄仅用于访问、调度协程,不会自动释放协程帧内存,需要手动调用
destroy()销毁。
Promise 对象是编译器自动在协程帧中创建的,我们外部无法直接获取它的地址,只能通过协程句柄间接访问、操作 Promise。
我们会把 std::coroutine_handle<promise_type> 定义为 Generator 类的成员变量,在 Generator 的构造函数中接收这个句柄完成初始化,这就使得每一个 Generator 对外对象,都持有对应协程的句柄,间接持有协程内部的 Promise。
示例里的 next()、value() 是我们自定义的对外 API,不属于标准强制要求的接口,是为了方便外部调用、简化协程操作手动封装的:
-
next():通过句柄调用resume()唤醒恢复挂起的协程; -
value():通过句柄拿到内部Promise对象,读取里面存储的产出值。
标准语法强制要求我们必须定义两套东西,缺一不可:
-
自定义
promise_type类型,严格实现标准规定的 6 个核心成员函数,函数名、参数、返回值不能随意修改; -
使用标准库模板
std::coroutine_handle<自定义Promise类型>得到协程句柄,作为对外返回类型(Generator)的成员。
编译器会自动识别这套固定结构,在协程创建、挂起、恢复、销毁的全生命周期自动调用 promise_type 内部的对应函数,少定义一个标准接口就会编译失败。
总结:
-
先自定义
promise_type内部类,实现全部标准规定的回调函数,作为协程的控制大脑,存储协程运行状态、产出数据; -
定义对外包装类(示例中的
Generator),内部持有std::coroutine_handle<promise_type>协程句柄,句柄用来访问、操控协程帧内的Promise; -
协程函数执行时,编译器自动创建协程帧、实例化
Promise,调用get_return_object()生成外部Generator对象返回给调用者; -
外部通过
Generator封装的next()、value()配合句柄完成协程唤醒、数据读取,依靠co_yield实现分次产出数据的生成器效果。
到这里我们已经理清了生成器样例的基础整体结构,对两大核心组件有了基础认知,下一部分我们继续逐行拆解代码,看每个 Promise 接口在协程运行的哪个阶段触发、分别起到什么作用。
生成器协程开始创建逻辑
核心围绕上面讲的两大组件:Promise 对象、协程句柄展开,把底层执行细节全部梳理清楚。 我们把协程的整个执行流程拆成三大阶段分步讲解,先重点拆解协程创建阶段,协程函数的创建、执行逻辑和普通函数有巨大区别,我们一步步对比区分。
协程调用的起点:main 函数执行 range 协程函数
程序从main函数顺序向下执行,当代码走到 auto gen = range(1,5); 这一行,调用协程函数range并传入参数。 这里和普通函数有本质区别:普通函数传参后会立刻进入函数内部从头到尾执行,协程函数传参完成后,不会立刻执行函数内部业务逻辑,编译器会先执行一整套协程初始化流程。
1. 第一步:在堆上分配协程帧(无栈协程核心特性)
C++20 协程属于无栈协程,协程创建时会在堆内存上开辟一块专属内存,叫做协程帧。
-
每一个正在运行的协程,都会对应独立的协程帧;协程执行完毕销毁后,对应的协程帧内存也会释放,不会持续占用内存;
-
协程函数本身的基础调用栈依然会正常创建,只是额外多了堆上的协程帧来存储协程专属状态;
-
协程帧是协程挂起、恢复的核心载体,协程暂停时,程序计数器、局部变量、寄存器、
Promise对象全部保存在这块堆内存中。
2. 第二步:在协程帧内自动创建 promise_type 对象
协程帧开辟完成后,编译器会自动实例化我们自定义的promise_type类型对象,并存放到协程帧内部:
-
promise_type只是行业通用命名,语法上我们可以随便命名类名,但带上promise单词可读性更强,其他人一眼就能识别这是协程控制类; -
这个类型有强制语法要求:必须实现标准规定的全套成员函数,少任意一个都会直接编译报错。原因是这些函数是编译器在协程生命周期不同阶段自动回调的,缺少对应函数就没有回调入口;
-
我们在
promise_type内部自定义了成员变量current_value,用来存放co_yield产出的数据:生成int就定义int、生成字符串就定义std::string、批量数据可以用容器,存储什么类型完全由业务需求自定义; -
编译器会调用
promise_type的默认构造函数,在协程帧内存中完成这个对象的初始化。
3. 第三步:调用 get_return_object() 生成协程对外返回对象
promise_type对象创建完成后,编译器会自动调用它的成员函数 get_return_object(),这个函数的核心作用是创建协程对外暴露的返回对象(示例中的Generator)。 这里我们理清三者的关联关系:
-
Generator是我们自定义的对外包装类,类名可以自定义,它内部持有第二大核心组件:std::coroutine_handle<promise_type>协程句柄; -
协程句柄本质可以理解为一个特殊指针,内部存储协程帧里
promise_type对象的地址。因为Promise对象存在堆上的协程帧中,外部代码无法直接访问,只能通过句柄间接操作; -
想要构造
Generator对象,必须传入合法的协程句柄,句柄则通过协程句柄的静态方法from_promise()获取。
from_promise () 静态方法逻辑
std::coroutine_handle 提供静态成员函数 from_promise(Promise& p),传入一个Promise对象的引用,就能生成绑定该 Promise 的协程句柄:
-
调用场景在
get_return_object()内部,this指针指向当前协程帧里的promise_type对象; -
执行
std::coroutine_handle<promise_type>::from_promise(*this),生成绑定当前 Promise 的句柄; -
把这个句柄传入
Generator的构造函数,完成Generator对外对象的创建,最终作为get_return_object()的返回值,交给 main 函数的gen变量接收。
有了Generator对象,我们就拥有了操控协程的入口:内部的协程句柄持有Promise对象的地址,既可以唤醒、暂停协程,也能读写 Promise 内部存储的产出值。
4. 第四步:调用 initial_suspend() 决定协程初始状态
Generator返回对象创建完成后,编译器会自动调用promise_type的initial_suspend()成员函数,这个函数用来控制协程创建完成后,是否立刻挂起。
单词suspend的含义是:暂停、挂起;initial代表协程初始化阶段;
该函数返回标准库内置的两种Awaitable类型对象,二选一:
-
std::suspend_always:always 代表 “总是”,协程创建完成后立刻挂起,不会执行协程内部任何业务代码(示例中使用的就是这个); -
std::suspend_never:never 代表 “从不”,协程创建后不挂起,直接往下执行协程函数内部逻辑,直到遇到第一个挂起点。
示例中 suspend_always 的执行效果
我们示例里initial_suspend()返回std::suspend_always{},协程走到这里直接挂起,执行权交还给外部main函数:
-
协程内部的
for循环完全不会执行,断点打在循环内也不会触发; -
协程创建、初始化整套流程全部走完,但协程停留在初始挂起点,等待外部调用
gen.next()手动唤醒恢复; -
如果换成
suspend_never,协程初始化完成后会直接进入循环执行,直到第一次碰到co_yield才会暂停。
总结:
-
外部调用协程函数
range(1,5),传入参数; -
编译器在堆上开辟协程帧,在帧内实例化
promise_type控制对象; -
自动调用
promise.get_return_object(),通过from_promise()生成协程句柄,构造对外Generator对象返回给调用方; -
自动调用
promise.initial_suspend(),根据返回值决定协程初始状态;示例返回suspend_always,协程直接挂起,回到 main 函数,初始化流程全部结束。
这套初始化流程是普通函数完全不存在的,也是协程难理解的核心原因,普通函数没有协程帧、Promise、句柄这一整套初始化逻辑,调用即执行;协程需要先完成底层初始化,再决定是否运行内部代码。
生成器协程中间迭代逻辑
initial_suspend是Promise类型强制要求实现的成员函数,编译器创建完Promise对象、生成外部Generator返回对象之后,会自动调用这个函数。它唯一的作用:协程帧、Promise 全部初始化完毕后,决定协程是立刻往下执行,还是先原地挂起等待外部唤醒。
-
返回
std::suspend_never:不挂起,协程直接向下执行内部代码,直到碰到第一个挂起点; -
返回
std::suspend_always:创建完成立刻挂起,也就是我们这份生成器示例的写法。
协程创建完成后直接挂起,执行权回到main函数,我们通过Generator对象的next()方法手动唤醒协程,先理清两个标准库协程句柄核心 API: 协程句柄std::coroutine_handle是标准库内置类型,自带两个关键成员函数:
-
done():判断协程是否执行完毕。协程运行中、挂起状态返回false;协程全部逻辑执行结束返回true; -
resume():唤醒挂起的协程,协程从上次暂停的位置继续执行,直到遇到下一个挂起点、函数执行完毕、抛出异常三者之一才会停下。
补充两个语法简化特性:
-
句柄重载了
operator(),直接调用handle()等价于handle.resume(); -
句柄重载了布尔转换
operator bool,可以直接放在if判断里,代表句柄是否有效。
再区分两个自定义类的强制 / 可选成员:
-
promise_type内部的所有函数(get_return_object、initial_suspend、yield_value等)是标准强制必须实现,缺一个编译报错; -
外层
Generator包装类,只强制要求持有协程句柄;next()、value()是我们为了简化外部调用自定义的辅助接口,不是标准强制要求,不同开源库会有不同封装写法。
第一次调用 gen.next () 完整执行流程
进入gen.next(),内部先执行!handle.done()判断:协程刚初始化挂起,还没有执行任何业务逻辑,done()返回false,取反结果为真,进入唤醒逻辑;
调用handle.resume()唤醒协程,底层会做两件事:
-
从堆上的协程帧恢复之前保存的运行上下文:局部变量
start、end、程序计数器、寄存器数据全部还原; -
CPU 根据恢复后的程序计数器地址,从协程之前挂起的位置(循环开头)继续执行代码,全程都在主线程运行。
协程内部开始执行循环:此时start=1,判断1 < 5条件成立,执行co_yield 1;;
编译器自动把co_yield 1翻译成对promise.yield_value(1)的调用:
-
把传入的数值
1存入promise内部的current_value成员变量; -
yield_value函数返回std::suspend_always{}匿名对象,代表执行完本次产出后立刻挂起协程;
触发新一轮挂起:
编译器把当前所有上下文(局部变量i、程序计数器、寄存器)重新保存回堆上的协程帧;
执行权交还给外部resume()调用处,resume()函数执行完毕;
回到next()函数内部,协程只是挂起、没有执行完毕,!handle.done()依旧为真,next()返回true,回到 main 的 while 循环;
外部调用gen.value()读取产出值:
-
通过句柄的
.promise()方法,获取协程帧里存储的promise_type对象; -
读取
promise.current_value里存放的1,返回给外部打印,控制台输出第一个数字1。
补充 suspend_always /suspend_never 语法说明
std::suspend_always、std::suspend_never都是标准库预定义的Awaitable类型,代码里写return {};是匿名对象简写,等价于构造临时实例,不需要额外定义变量,只要返回这两个类型,编译器就能识别挂起策略。
打印完数字 1 后,回到while(gen.next())循环条件,重复整套流程:
-
再次进入
next(),done()依旧为false,调用resume()唤醒协程; -
协程从上次
co_yield挂起的位置恢复,执行循环自增i++,i变为 2; -
判断
2 < 5成立,执行co_yield 2,调用promise.yield_value(2)把 2 存入current_value,再次返回suspend_always挂起; -
执行权回到外部
next(),返回true,调用gen.value()读取 2 并打印; -
重复这套逻辑,依次产出、打印 3、4;
整套生成器的核心特点:按需生成、惰性产出。不会一次性把 1~4 全部计算出来存到容器里,而是每次外部调用next()才唤醒协程计算一个值,产出后立刻挂起等待读取,节约内存,适合海量数据迭代场景。
总结:
-
协程创建完成后,
initial_suspend()返回suspend_always,初始直接挂起,不会自动运行内部循环; -
外部循环调用
gen.next(),内部通过协程句柄resume()唤醒暂停的协程; -
协程运行到
co_yield时,将数值存入Promise对象,随后主动挂起,交还执行权; -
外部通过
gen.value()从Promise中读取刚产出的数值并打印; -
只要协程没有执行完毕,
next()就返回 true,循环持续唤醒、产出、读取;直到循环条件不成立,协程执行完毕,done()返回 true,循环终止。
生成器协程结束与销毁逻辑
接下来我们梳理生成器协程完整的收尾、结束、销毁全流程。
我们承接上一段流程,此时程序已经打印出数字 4,协程在产出 4 后触发co_yield再次挂起,执行权回到main函数的while(gen.next())循环处,我们从这一步开始完整拆解协程收尾全过程。
最后一次调用 gen.next (),协程走到循环末尾
-
打印完数字 4 后,回到 while 循环条件,再次执行
gen.next(); -
进入
next()内部,调用handle.done()判断协程状态:当前协程仅挂起、未执行完毕,返回false,因此执行handle.resume()唤醒协程; -
协程从上次
co_yield的挂起点恢复执行,执行循环自增i++,i从 4 变成 5; -
执行循环判断
i < 5,5 不小于 5,循环条件不成立,for循环直接结束; -
协程函数内部没有剩余业务代码,走到函数最末尾,普通函数走到末尾直接销毁栈帧,但协程会触发一套固定的收尾回调流程。
协程函数末尾:自动调用 promise.return_void ()
协程执行到函数末尾,编译器会自动隐式插入co_return;,随后自动调用promise_type的return_void()成员函数:
语法区分规则:
-
协程末尾无返回值、仅隐式
co_return;,必须在promise_type中实现return_void(); -
如果代码写了
co_return 数值/对象;带返回值,就不能写return_void(),需要实现return_value(T val),参数类型和返回值类型匹配,编译器会把返回值传入该函数,存入promise内部变量,外部后续可以读取;
本示例中return_void()内部没有任何业务逻辑,仅满足标准语法强制要求,执行完直接返回;
执行完return_void()后,编译器会将协程全局状态标记为执行完成,此时调用handle.done()会返回true。
return_void()执行完毕后,编译器自动调用promise_type的final_suspend(),这是协程生命周期倒数第二个核心回调,作用和initial_suspend()对应:
-
initial_suspend():协程创建初始化完成后,控制执行前是否挂起; -
final_suspend():协程全部业务逻辑执行完毕后,控制执行结束后是否挂起。
该函数同样只能返回两种标准库Awaitable类型:
-
std::suspend_never:协程结束后不挂起,直接自动销毁堆上的协程帧内存。 注意风险:协程帧销毁后,内部Promise对象、存储的返回值全部失效,外部无法再通过句柄读取协程产出 / 返回的数据; -
std::suspend_always(本示例采用):协程执行完毕后依旧挂起,不会自动销毁协程帧。 优势:协程帧、Promise 对象会保留在堆内存,外部只要持有协程句柄,依旧可以读取最终返回值; 代价:内存不会自动释放,必须手动调用句柄的destroy()方法销毁协程帧,否则造成内存泄漏。
final_suspend()返回std::suspend_always{},协程执行完所有逻辑后再次进入挂起状态,执行权交还给外部resume()调用位置。
回到 next (),循环终止跳出 while
-
resume()执行完毕,回到next()函数内部; -
再次执行
handle.done()判断,此时协程已经执行完毕,返回true; -
next()函数返回!true也就是false; -
while(false)条件不成立,循环直接终止,不再执行读取value()打印的逻辑。
循环跳出后,代码继续向下执行,直到main函数运行结束,局部变量gen(Generator 类型对象)离开作用域,触发Generator的析构函数:
-
Generator析构函数内部会判断内部协程句柄是否有效; -
因为我们
final_suspend()返回的是suspend_always,协程帧没有自动销毁,析构函数会手动调用句柄的handle.destroy(); -
destroy()底层操作:释放堆上分配的协程帧内存,销毁帧内部存储的promise_type对象、局部变量、程序计数器上下文等全部数据; -
协程帧内存完全释放后,这个协程完整生命周期正式彻底结束,不存在任何内存残留。
补充:如果 final_suspend 返回 suspend_never
如果我们把final_suspend()改成返回std::suspend_never: 协程执行完return_void()后直接销毁协程帧,不需要手动调用destroy(),但协程一旦结束,外部无法再读取任何协程产出、返回的数据,适合执行完不需要留存结果的场景。
结合本次流程,重新理解 C++20 无栈协程核心特性
我们全程示例都是 C++20 标准的无栈协程,这里结合完整执行流程,通俗解释无栈协程的底层原理:
无栈协程不会为每个协程单独分配独立栈空间,协程函数本身的局部变量、执行上下文不会长期存放在线程栈;
线程栈是调用方共用的:谁调用resume()唤醒协程,协程恢复运行时,临时变量就存放在当前唤醒线程的栈上;
协程触发挂起时,编译器会把需要保留的数据(局部变量、程序计数器、寄存器状态)全部拷贝到堆上的协程帧中保存,清空线程栈上临时数据,线程可以继续执行其他逻辑;
下次唤醒协程时,再从堆协程帧读取保存的上下文,恢复到当前线程栈,从上次挂起位置继续执行;
优势:仅在堆上按需存储少量上下文,内存占用极低,上下文切换全程用户态完成,没有内核态切换开销,切换成本远低于有栈协程、系统线程。
各阶段详细拆解
下面我们整体来过一遍
1. 协程创建阶段(触发:auto gen = range(1,5))
核心逻辑:调用 range 协程函数时,编译器自动创建协程帧(存储局部变量 i、参数 start=1/end=5、promise_type 实例),并通过 promise 衔接 Generator。
代码映射:
i. 编译器自动构造 promise_type 实例(初始化 current_value);
ii. 调用 promise.get_return_object():创建 Generator 对象,绑定 coroutine_handle<promise_type>(通过 from_promise(*this) 关联 promise);
iii. Generator 构造函数执行:保存 handle(后续控制协程的 “手柄”)。

2. 初始挂起状态(触发:initial_suspend())
核心逻辑:协程创建后不立即执行,而是先挂起(suspend_always 表示 “总是挂起”),实现惰性执行(按需启动)。
代码映射:
promise.initial_suspend() 返回 std::suspend_always{},协程暂停在 “执行前” 状态;
如果promise.initial_suspend() 返回 std::suspend_nerver{},那么协程将不会挂起,继续执行,知道运行到相关挂起点;
此时 main 中拿到的 gen 已就绪,但 range 的 for 循环尚未开始执行。

3. 恢复执行阶段(触发:gen.next())
核心逻辑:外部通过 Generator 的 next() 接口,用 handle 唤醒协程,执行到下一个 co_yield 或协程结束。
代码映射:
i. gen.next() 判断 !handle.done()(当前协程未结束),调用 handle.resume();;
ii. 协程从挂起点恢复,执行 range 的逻辑:
第一次恢复:进入 for 循环,i=start=1,执行 co_yield 1;;
后续恢复:从 co_yield 的挂起点继续,i++(如 i=2),再次执行 co_yield 2,直到 i >= end(i=4 后 i++=5,退出循环)。

4. 产出值挂起状态(触发:co_yield)
核心逻辑:co_yield 是协程 “产出值” 的关键字,本质是调用 promise.yield_value(),并挂起协程,让外部获取产出值。
代码映射:
i. co_yield value → 调用 promise.yield_value(value),将 value 存入 promise.current_value;
ii. yield_value() 返回 std::suspend_always{},协程再次挂起;
iii. 此时 gen.next() 返回 !handle.done()(true),外部通过 gen.value() 读取 promise.current_value(如第一次读取 1),并打印。

5. 协程结束状态(触发:for 循环退出)
核心逻辑:range 的 for 循环执行完毕(i=5 >= end=5),协程无更多代码可执行,进入结束流程。
代码映射:
协程执行到函数末尾,编译器自动调用 promise.return_void()(处理 co_return; 无值返回的场景);
此时协程标记为 “已结束”(handle.done() 变为 true)。

6. 最终挂起状态(触发:final_suspend())
核心逻辑:协程结束后,通过 final_suspend() 挂起,等待外部手动销毁(避免协程提前释放)。
代码映射:
promise.final_suspend() 返回 std::suspend_always{},协程保持挂起状态;
此时 gen.next() 再次调用时,handle.done() 为 true,next() 返回 false,main 的 while 循环退出。

7. 协程销毁阶段(触发:gen 析构)
核心逻辑:main 函数结束,Generator 对象 gen 出作用域,析构函数释放协程帧内存。
代码映射:
Generator::~Generator() 判断 handle 有效,调用 handle.destroy();;
销毁协程帧(释放 i、start、end、promise_type 实例的内存),协程生命周期完全结束。

前面我们完整走完了生成器协程从创建、运行、产出数据、结束销毁的全流程,这里再着重提醒大家:一定要自己手绘完整执行流程图,理清Promise对象与协程句柄的配合逻辑。吃透这个生成器示例,才算正式入门 C++20 协程,这份示例是所有后续内容的基础,必须吃透。
下面我们补充三大块内容:
-
Promise 全套强制接口完整作用,重点讲解上面没细说的协程异常处理机制;
-
initial_suspend、final_suspend分别返回suspend_always/suspend_never的行为差异、踩坑案例; -
co_return带返回值的使用方式;
Promise 全部强制接口完整梳理
自定义promise_type是协程核心控制载体,所有协程运行状态、产出数据、异常、生命周期全部由它管控,所有强制要求实现的函数作用汇总:
get_return_object():编译器创建 Promise 对象后第一时间调用,通过std::coroutine_handle<>::from_promise(*this)生成绑定当前协程的句柄,再用句柄构造对外的包装类(示例中的Generator)返回给调用方;
initial_suspend():协程初始化完成后执行,控制协程创建完成后是否立刻挂起;
final_suspend():协程全部业务逻辑执行完毕后执行,控制协程结束后是否挂起、是否自动销毁协程帧;
yield_value(T val):仅使用co_yield时必须实现,接收产出数值,存入 Promise 内部成员变量供外部读取;
return_void() / return_value(T val):二选一实现。无返回值隐式co_return;实现return_void();带返回值co_return xxx;实现return_value(T),存储协程最终返回结果;
unhandled_exception():协程内部抛出且未被捕获的异常,会自动进入该函数处理,是协程异常中转核心入口。
重点补充:协程完整异常处理流程
之前示例为简化逻辑,在unhandled_exception()中直接终止程序,实际工业开发需要将异常中转到外层调用代码捕获,整套机制依赖std::current_exception、std::exception_ptr、std::rethrow_exception三个标准库工具。
核心工具介绍
-
std::current_exception:C++11 引入,仅在异常处理上下文(catch 块、unhandled_exception)中调用,获取当前正在处理的异常,返回std::exception_ptr; -
std::exception_ptr:异常智能指针,可以存储任意类型的异常对象(int、string、标准异常类均可)。底层会在堆上拷贝 / 移动异常对象,自动管理内存,无需手动释放;内部本质存储可能是void*通用指针,记录异常对象地址与原始类型信息; -
std::rethrow_exception:接收exception_ptr,恢复原始异常类型并重新抛出,让外层代码可以正常 catch 捕获对应异常。
完整异常存储与抛出流程
协程执行过程中抛出异常,内部无匹配 catch 捕获,编译器自动调用promise.unhandled_exception();
在该函数内调用std::current_exception(),生成存储当前异常的exception_ptr;
在promise_type中新增std::exception_ptr m_excep;成员变量,将异常指针存入 Promise;
协程保存上下文后挂起,执行权回到外部调用resume()的位置;
外部调用gen.next()唤醒协程后,在resume()执行完毕立刻判断 Promise 内部异常指针是否为空;
若存在异常,调用std::rethrow_exception(m_excep)重新抛出异常,外层try-catch即可捕获协程内部产生的异常。
实操演示逻辑 我们可以改造示例代码,在i == 3时抛出std::out_of_range异常:
#include <coroutine>
#include <exception>
#include <iostream>
// 1. 协程的返回类型
struct Generator {
// 2. 核心:promise_type
struct promise_type {
int current_value; // 用于存储产出的值
std::exception_ptr m_excep; // 用于保存异常对象
// 2a. 创建返回对象
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 2b. 初始挂起:选择挂起,让协程惰性执行
std::suspend_always initial_suspend() { return {}; }
// 2c. 最终挂起:选择挂起,我们需要手动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2d. 处理 co_yield
std::suspend_always yield_value(int value) {
current_value = value;
return {}; // 产出后总是挂起
}
// 2e. 处理 co_return; (无值返回)
void return_void() {}
// 2f. 处理异常
void unhandled_exception() {
// std::terminate();
m_excep = std::current_exception();
}
};
// 3. Generator 类本身的成员
// 协程句柄,用于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数和析构函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() {
if (handle) handle.destroy(); // 负责销毁协程帧
}
// 4. 提供给外部的API
// 获取当前值
int value() const {
return handle.promise().current_value;
}
// 恢复执行直到下一个co_yield或结束
bool next() {
if (!handle.done()) {
handle.resume();
if (handle.promise().m_excep) {
std::rethrow_exception(handle.promise().m_excep);
}
}
return !handle.done();
}
};
// 使用这个生成器的协程函数
Generator range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // 这会调用 promise.yield_value(i)
if (i == 3) {
throw std::out_of_range("测试异常");
}
}
// 协程结束,调用 promise.return_void()
}
int main() {
auto gen = range(1, 5); // 创建协程,initial_suspend挂起,此时协程未执行
try {
while (gen.next()) { // 恢复协程,协程执行到co_yield处挂起
std::cout << gen.value() << " "; // 从promise中获取产出的值
}
}
catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
}
-
协程先正常产出 1、2,打印完成;
-
再次调用
next()唤醒协程,执行co_yield 3,存入数值 3 后挂起,打印 3; -
再次调用
next()唤醒协程,i 自增为 4,循环执行到下一次co_yield前抛出异常; -
异常未捕获,进入
unhandled_exception(),异常存入 Promise; -
执行权回到
next()内部,检测到存在异常指针,调用rethrow_exception抛出; -
外层 main 函数的 catch 块捕获该异常,终止循环。
1 2 3 测试异常
E:\myLearn\coroutine\x64\Debug\coroutine.exe (进程 30652)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
整个逻辑核心:协程内部数据、异常全部存放在协程帧内的 Promise 对象中,外部只能通过协程句柄访问 Promise,间接读取数值、异常。
initial_suspend有两种标准返回类型:std::suspend_always、std::suspend_never。
-
std::suspend_always(示例默认写法) 协程创建、Promise、Generator 对象全部构造完成后,直接挂起,不会执行协程内部任何代码,执行权直接返回 main 函数。 外部第一次调用gen.next()才会唤醒协程,执行循环产出第一个数值。 -
std::suspend_never协程初始化完成后不挂起,直接向下执行内部代码,直到遇到第一个co_yield挂起点才暂停。 对应示例会出现特殊现象:创建gen对象时直接执行到co_yield 1,数值 1 存入 Promise 并挂起;但代码紧接着进入while(gen.next()),next()内部再次调用resume()唤醒协程,i 自增为 2,直接覆盖 Promise 内存储的值,外部第一次调用gen.value()读取到的是 2,丢失数值 1。
适用场景区分:
-
业务需要外部主动控制协程启动时机,使用
suspend_always; -
协程创建后需要预先执行一段初始化逻辑、产出首个数据,可使用
suspend_never。
final_suspend是协程执行完毕后的收尾控制函数,两种返回值会带来完全不同的内存与访问风险,也是开发高频踩坑点。
std::suspend_always(生成器场景推荐)协程全部逻辑执行完成后依旧挂起,不会自动销毁堆上的协程帧。
-
优势:协程帧、Promise 对象持续保留,循环结束后外部仍可以通过句柄读取最终返回值、存储的异常;
-
代价:内存不会自动释放,需要在外层
Generator包装类的析构函数中调用handle.destroy()手动释放协程帧,避免内存泄漏。
std::suspend_never(生成器场景禁止使用) 协程执行完毕后直接销毁堆上协程帧,Promise 对象、局部变量、异常指针全部释放。
-
致命问题:协程句柄内部仅存储指向协程帧的裸指针,帧销毁后指针变为野指针;后续代码调用
handle.done()、handle.promise()、gen.value()都会访问野内存,直接程序崩溃,读取到随机垃圾值。 -
仅适用场景:协程执行结束后不需要读取任何返回值、异常,且不会再通过句柄访问协程。
co_return 带返回值的使用规则
隐式无返回值:协程函数走到末尾等价于co_return;,Promise 必须实现return_void(),无额外数据存储;
显式带返回值 co_return val;:
-
Promise 不能实现
return_void(),必须实现return_value(T),参数类型和返回值类型完全匹配; -
返回值会存入 Promise 自定义成员变量(如
std::string m_ret、int m_result); -
必须搭配
final_suspend返回suspend_always,协程帧不销毁,循环结束后外部才能通过句柄访问 Promise 拿到最终返回值; -
支持任意类型返回值:int、string、容器、自定义类都可以。
注意:
handle.resume() 与 handle() 关系 二者功能完全等价,调用handle()仿函数会底层转发执行resume(),用来唤醒挂起的协程。
句柄布尔转换 operator bool 作用
-
错误说法:布尔转换判断协程是否执行完毕;
-
正确定义:仅判断句柄本身是否有效,即内部存储的协程帧指针是否为空。
通过from_promise绑定真实协程的句柄:指针有效,返回 true;
默认构造、空指针构造的句柄:指针为空,返回 false;
判断协程是否执行完毕专用接口:handle.done(),和布尔转换完全无关,二者用途不能混淆。
协程句柄通用底层转换原理 std::coroutine_handle<T> 可以无损耗转换为无类型std::coroutine_handle<void>:
底层统一存储void*通用指针,屏蔽不同 Promise 类型的差异;
作用:统一调度函数,接收任意不同 Promise 类型的协程句柄,无需模板重载,为Awaitable异步调度提供基础支撑。
协程生成器变形写法【迭代器适配】与 C++23 标准生成器
核心新增能力:给 Generator 包装类增加迭代器支持,实现范围for循环遍历生成器,不用再手动循环调用next()、value()。同时顺带对比 C++23 标准库内置std::generator,解释学习底层原生协程的实际意义。
我们之前操作生成器,只能手动写 while 循环:调用gen.next()判断是否还有数据,再调用gen.value()读取数值,写法繁琐。 如果给自定义Generator类增加迭代器支持,就能直接使用 C++ 范围 for 语法遍历:
for (auto v : gen) {
cout << v << endl;
}
从底层语法来讲,范围 for 会编译器自动转换成迭代器遍历逻辑:调用begin()拿到起始迭代器、end()拿到末尾迭代器,循环判断it != end,通过解引用*it取值,迭代器自增++it推进遍历。
所以我们只需要给 Generator 实现迭代器配套接口,就能让编译器自动完成这套转换,简化上层使用。
原有 Generator、promise_type、协程句柄逻辑完全不变,仅新增迭代器内部类、begin()、end()三个配套内容。
#include <coroutine>
#include <exception>
#include <iostream>
template<class T>
// 1. 协程的返回类型
struct Generator {
// 2. 核心:promise_type
struct promise_type {
T current_value; // 用于存储产出的值
std::exception_ptr m_excep; // 用于保存异常对象
// 2a. 创建返回对象
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 2b. 初始挂起:选择挂起,让协程惰性执行
std::suspend_always initial_suspend() { return {}; }
// 2c. 最终挂起:选择挂起,我们需要手动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2d. 处理 co_yield
std::suspend_always yield_value(T value) {
current_value = value;
return {}; // 产出后总是挂起
}
// 2e. 处理 co_return; (无值返回)
void return_void() {}
// 2f. 处理异常
void unhandled_exception() {
// std::terminate();
m_excep = std::current_exception();
}
};
// 【新增点】迭代器
struct iterator {
std::coroutine_handle<promise_type> handle;
// 构造函数
iterator(std::coroutine_handle<promise_type> h = nullptr) : handle(h) {}
// 重载 operator++
iterator& operator++() {
if (handle && !handle.done()) {
handle.resume();
}
return *this;
}
// 重载 operator*
const T& operator*() const {
return handle.promise().current_value;
}
// 重载 operator!=[方式一]
//bool operator!=(const iterator& other) const {
// return !handle.done();
//}
// 重载 operator!=[方式二]
bool operator!=(const iterator& other) const {
return handle != other.handle && !handle.done();
}
};
iterator begin() {
if (handle && !handle.done()) {
handle.resume();
}
return iterator(handle);
}
iterator end() {
return iterator();
}
// 3. Generator 类本身的成员
// 协程句柄,用于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数和析构函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() {
if (handle) handle.destroy(); // 负责销毁协程帧
}
// 4. 提供给外部的API
// 获取当前值
int value() const {
return handle.promise().current_value;
}
// 恢复执行直到下一个co_yield或结束
bool next() {
if (!handle.done()) {
handle.resume();
if (handle.promise().m_excep) {
std::rethrow_exception(handle.promise().m_excep);
}
}
return !handle.done();
}
};
// 使用这个生成器的协程函数
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // 这会调用 promise.yield_value(i)
if (i == 3) {
//throw std::out_of_range("测试异常");
}
}
// 协程结束,调用 promise.return_void()
}
int main() {
auto gen = range(1, 5); // 创建协程,initial_suspend挂起,此时协程未执行
try {
std::cout << "非迭代器版本" << std::endl;
while (gen.next()) { // 恢复协程,协程执行到co_yield处挂起
std::cout << gen.value() << " "; // 从promise中获取产出的值
}
std::cout << std::endl;
std::cout << "迭代器版本[语法糖]" << std::endl;
for (auto v : range(1, 5)) {
std::cout << v << " ";
}
std::cout << std::endl;
std::cout << "迭代器版本[底层]" << std::endl;
auto gen_3 = range(1, 5);
for (auto it = gen_3.begin(); it != gen_3.end(); ++it) {
std::cout << *it << " ";
}
}
catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
}
非迭代器版本
1 2 3 4
迭代器版本[语法糖]
1 2 3 4
迭代器版本[底层]
1 2 3 4
1. 内嵌迭代器类型
在 Generator 内部定义迭代器嵌套类,迭代器内部唯一成员:std::coroutine_handle<promise_type> m_handle,也就是协程句柄;
迭代器默认构造函数:构造空句柄(内部指针为空,对应end()末尾迭代器);
带句柄参数的构造:绑定真实协程句柄,对应begin()返回的有效迭代器。
2. Generator::begin () 起始迭代器
调用begin()时会做两件关键操作:
用内部协程句柄执行resume()唤醒协程,协程运行到第一个co_yield,产出第一个值存入 promise 对象,随后挂起;
用当前有效句柄构造迭代器并返回,作为遍历起点。
3. Generator::end () 末尾迭代器
直接调用迭代器默认构造,生成持有空句柄的迭代器,仅作为遍历终止标记,不会操作协程。
4. 迭代器重载运算符(遍历核心)
解引用运算符 operator* 通过迭代器内部句柄访问.promise(),取出 promise 里存储的生成值,等价于原来的gen.value()。
自增运算符 operator++ 迭代器自增代表取下一个生成值:调用句柄resume()恢复协程,执行到下一个co_yield存入新值并挂起; 执行完成后判断handle.done(),如果协程已经全部执行完毕,可以主动把迭代器内部句柄置空,让它和 end 空迭代器相等。
不等比较运算符 operator!= 两种主流实现思路,都能正常运行,只是可读性有区别:
写法 1(简易版):不对比句柄是否相等,直接判断当前迭代器句柄!handle.done(),协程未结束就继续循环;缺点是有效句柄永远和 end 空句柄不相等,逻辑不直观。
写法 2(规范自洽版):协程执行完毕后,把迭代器内部句柄置为空;比较时直接判断两个迭代器内部句柄是否一致,协程结束后迭代器和 end 完全相等,逻辑更通顺。
两种写法底层运行效果一致,只是终止判断的实现方式不同,业务上任选其一即可。
前面反复提到 C++20 协程只提供底层基础组件,需要手写大量 promise、句柄、迭代器模板代码,而 C++23 直接在标准库内置了std::generator:std::generator - cppreference.com
使用方式:引入对应标准头文件,无需自己实现 promise、Generator、迭代器整套代码,开箱即用;
底层本质:标准库内部已经封装好我们今天手写的全部逻辑 ——promise 类型、协程句柄、迭代器适配、异常存储、挂起控制;
代码对比:C++23 标准生成器代码极简,只需要写业务循环和 co_yield,上层直接范围 for 遍历;
#include <generator> // C++23 标准库提供的协程生成器
#include <iostream>
// 定义一个二叉树结构
template<typename T>
struct Tree
{
T value; // 当前节点的值
Tree* left{}, * right{}; // 左子树和右子树指针,默认初始化为 nullptr
// 中序遍历:返回一个生成器,每次 yield 一个节点的值(常量引用)
// 使用 const 修饰,表示遍历时不会修改树的结构
std::generator<const T&> traverse_inorder() const
{
// 1. 先遍历左子树
if (left)
// co_yield std::ranges::elements_of 是 C++23 的新语法
// 作用:将左子树生成器产生的所有元素"扁平化"地 yield 出来
// 相当于:for (const T& v : left->traverse_inorder()) co_yield v;
co_yield std::ranges::elements_of(left->traverse_inorder());
// 2. 然后 yield 当前节点的值
co_yield value;
// 3. 最后遍历右子树
if (right)
// 同样,将右子树生成器的所有元素展开 yield
co_yield std::ranges::elements_of(right->traverse_inorder());
}
};
int main()
{
// 使用数组静态初始化一棵完全二叉树
// 树的结构:
// 'D'
// / \
// 'B' 'F'
// / \ / \
// 'A' 'C' 'E' 'G'
//
// 数组索引: 0:D, 1:B, 2:F, 3:A, 4:C, 5:E, 6:G
Tree<char> tree[]
{
// 根节点:索引0,值为'D',左子树指向索引1,右子树指向索引2
{'D', tree + 1, tree + 2},
// 左子树的根:索引1,值为'B',左子树指向索引3,右子树指向索引4
{'B', tree + 3, tree + 4},
// 右子树的根:索引2,值为'F',左子树指向索引5,右子树指向索引6
{'F', tree + 5, tree + 6},
// 叶子节点们:索引3~6,左右子树均为 nullptr
{'A'}, // 索引3:'B'的左孩子
{'C'}, // 索引4:'B'的右孩子
{'E'}, // 索引5:'F'的左孩子
{'G'} // 索引6:'F'的右孩子
};
// 从根节点开始中序遍历
// tree 是数组,会退化为指向第一个元素的指针,即根节点
// traverse_inorder() 返回一个生成器,支持范围 for 循环
for (char x : tree->traverse_inorder())
std::cout << x << ' '; // 预期输出:A B C D E F G
std::cout << '\n';
}
版本限制:当前多数编译器(如旧版 MSVC、GCC)还未完整支持 C++23 全部协程配套库,需要升级最新编译器才能编译运行;
标准迭代规划:
-
C++20:仅提供无栈协程底层语法,无任何上层封装;
-
C++23:仅补充基础
std::generator生成器,只覆盖最简单的同步生成场景; -
C++26:计划完善协程标准库,新增任务、调度器、异步相关封装,协程使用门槛会大幅降低。
C++20 协程第三关键字 co_await 与 Awaitable
接下来我们学习协程第三个核心关键字:co_await。前面我们已经讲完 co_yield、co_return,但co_await 才是协程里最重要的关键字。
协程的核心特性是可暂停、可恢复,所有暂停、恢复逻辑全部依赖 co_await 和配套的 Awaitable 对象。之前讲解生成器时,我刻意简化了底层细节,分阶段带大家理解,现在补上完整底层原理。 之前生成器里一共有三处会触发协程挂起:
-
initial_suspend()初始挂起; -
yield_value()产出数据后挂起; -
final_suspend()协程执行完毕收尾挂起。 这三个函数的返回值(suspend_always/suspend_never)本质都是标准库提供的Awaitable对象,也就是协程第三大核心组件。
什么是 Awaitable 对象?
定义:紧跟在 co_await 关键字后面的表达式,必须是一个 Awaitable 类型对象;
判定规则:只要一个类完整实现了三个固定成员函数,这个类的实例就是合法 Awaitable 对象,语法上没有强制类名要求;
核心作用:全权决定 co_await 执行时协程是否需要暂停,同时提供暂停前、恢复后的自定义回调逻辑。
一个标准 Awaitable 类型必须实现三个成员函数,分别对应协程等待全流程的三个阶段:await_ready、await_suspend、await_resume。编译器会按固定顺序调用这三个函数,我们逐个拆解作用。
1. await_ready ():判断是否需要暂停协程
函数返回布尔值,是整个等待逻辑的开关:
-
返回
true:代表 “就绪,无需等待”,协程不暂停,直接跳过挂起流程,只执行最后一步await_resume; -
返回
false:代表 “未就绪,需要等待”,继续执行后续挂起流程,调用await_suspend,完成后协程挂起。
绝大多数业务场景下,我们都会返回 false,使用协程的初衷就是等待某个事件、IO 完成后再恢复执行,所以需要暂停。
2. await_suspend (协程句柄):暂停前预处理逻辑
仅当 await_ready 返回 false 时才会调用,执行时机是协程即将挂起之前,所有等待前置操作都写在这里。 入参是无类型通用协程句柄 std::coroutine_handle<void>,所有不同 Promise 类型的协程句柄都可以自动转换成该类型,通用性极强。
核心业务用途(以网络 IO 举例)
比如网络服务里的阻塞 IO 场景: 普通同步 read / accept 会阻塞整个线程,没有数据、没有新连接时线程卡死; 使用协程 + Awaitable 改造后:
-
在
await_suspend中把当前套接字注册到epoll/poll多路复用器; -
注册完成后协程直接挂起,让出线程执行权,线程可以去处理其他业务;
-
等到 IO 事件就绪(数据到达 / 新连接到来),外部调度器拿到句柄,调用
resume()恢复协程。
简单总结:await_suspend 的核心工作就是安排好谁、在什么时机恢复当前协程。
await_suspend 三种合法返回值,对应不同执行逻辑
-
返回
void(最常用) 协程挂起后,执行权直接交还给调用当前协程的上层代码,是同步生成器、基础异步 IO 的默认写法。 -
返回
bool精细化控制挂起行为:返回true正常挂起;返回false放弃挂起,协程立刻继续向下执行。日常开发使用极少。 -
返回另一个协程句柄
std::coroutine_handle<>实现协程对称转移:当前协程挂起后,执行权直接切换到返回的目标协程,实现无栈链式调度,多用于异步任务调度框架。
补充:该函数一般不会抛出异常,建议加上 noexcept 修饰。
3. await_resume ():协程恢复后的收尾、返回结果
两种场景会触发该函数调用:
-
await_ready返回true,全程没有挂起,直接执行; -
协程外部被
resume()唤醒,挂起结束后第一时间执行。
函数返回值就是整个 co_await 表达式 的最终结果:
-
返回
void:co_await语句无返回值; -
返回任意自定义类型:可以接收 IO 读取的数据、任务执行结果,写为
auto res = co_await io_read();。
co_await 底层编译转换逻辑
只要代码里写 co_await expr;,编译器会自动把这段代码翻译成固定逻辑伪代码,三个 Awaitable 函数按顺序调度,且不是三个函数一定会全部执行:
auto&& await_obj = expr;
if (!await_obj.await_ready())
{
await_obj.await_suspend(handle);
// 协程在此处挂起,交出执行权
}
return await_obj.await_resume();
执行分支区分:
-
await_ready() == true:跳过await_suspend,不挂起,直接执行await_resume; -
await_ready() == false:先执行await_suspend,随后协程挂起;外部唤醒后,再执行await_resume。
标准库内置两个基础 Awaitable:suspend_always /suspend_never
之前生成器中反复用到的两个挂起标记,本质就是标准库预定义好的 Awaitable 类型,底层逻辑完全贴合上面的三函数规范:
std::suspend_always
-
await_ready()返回false→ 必须走挂起流程; -
await_suspend空实现,返回void; -
await_resume空实现,无返回值;
效果:只要返回该对象,协程一定会挂起,执行权回到上层调用处。
std::suspend_never
-
await_ready()返回true→ 直接跳过挂起逻辑; -
不会调用
await_suspend,直接执行await_resume; 效果:协程完全不暂停,一路向下执行。
回头拆解生成器所有挂起的底层本质
之前讲生成器时,只告诉大家返回 suspend_always 就会挂起,现在结合 co_await 补齐完整底层转换逻辑,所有挂起本质都是编译器自动生成 co_await 语句:
1. initial_suspend () 底层转换
initial_suspend() 返回 suspend_always,编译器等价生成:
co_await promise.initial_suspend();
运行流程:【本质就是返回一个 awaitable 对象,执对象内部规定的逻辑】
-
suspend_always::await_ready()返回 false; -
调用
await_suspend,无额外逻辑; -
协程直接挂起,主线程拿到 Generator 对象,协程内部代码完全不执行。
2. co_yield 语句底层转换
co_yield val; 不是单独语法,编译器会分步转换:
-
调用
promise.yield_value(val),把数值存入 Promise 对象; -
yield_value返回suspend_always; -
自动生成
co_await 该返回对象;; -
触发 Awaitable 完整流程,协程挂起,等待外部
next()唤醒。
3. final_suspend () 底层转换
和 initial_suspend 逻辑完全一致,编译器自动生成:
co_await promise.final_suspend();
协程全部业务执行完毕后执行,控制结束后是否挂起、是否销毁协程帧。
协程学习样例之异步线程恢复
这个样例展示了co_await的使用方法,其次就是展示了,协程可以在另一个线程中恢复执行后续逻辑
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 增强的可等待对象,添加更多日志
struct AsyncOperation {
int value;
bool ready = false;
const char* name;
AsyncOperation(int v, bool r, const char* n = "")
: value(v), ready(r), name(n) {
std::cout << "[" << name << "] AsyncOperation 构造: value=" << value
<< ", ready=" << ready << " (线程: " << std::this_thread::get_id() << ")"
<< std::endl;
}
bool await_ready() const noexcept {
std::cout << "[" << name << "] await_ready() 调用,返回: " << ready << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return ready;
}
void await_suspend(std::coroutine_handle<> handle) noexcept {
std::cout << "[" << name << "] await_suspend() 调用,协程句柄: " << handle.address() << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 启动异步操作
std::thread([this, handle, name = this->name]() {
std::cout << "[" << name << "] 异步线程开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
this->ready = true;
std::cout << "[" << name << "] 异步操作完成,准备恢复协程 " << "(线程: " << std::this_thread::get_id() << ")" << std::endl;
// 恢复协程执行
handle.resume();
std::cout << "[" << name << "] 异步线程结束 " << "(线程: " << std::this_thread::get_id() << ")" << std::endl;
}).detach();
std::cout << "[" << name << "] await_suspend() 返回,协程即将挂起 " << "(线程: " << std::this_thread::get_id() << ")" << std::endl;
}
int await_resume() noexcept {
std::cout << "[" << name << "] await_resume() 调用,返回结果: " << value << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return value;
}
};
struct Task {
struct promise_type {
int current_value;
Task get_return_object() {
std::cout << "promise_type::get_return_object() 调用" << std::endl;
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() noexcept {
std::cout << "promise_type::initial_suspend() 调用 - 立即开始执行" << std::endl;
return {};
}
std::suspend_always final_suspend() noexcept {
std::cout << "promise_type::final_suspend() 调用 - 协程结束" << std::endl;
return {};
}
void unhandled_exception() {
std::cout << "promise_type::unhandled_exception() 调用" << std::endl;
}
void return_void() {
std::cout << "promise_type::return_void() 调用 - 协程正常返回" << std::endl;
}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {
std::cout << "Task 对象构造,句柄: " << handle.address() << std::endl;
}
~Task() {
if (handle) {
std::cout << "Task 析构,销毁协程句柄" << std::endl;
handle.destroy();
}
}
};
Task detailed_async_example() {
std::cout << "=== 协程函数开始执行 ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第一次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result1 = co_await AsyncOperation{ 100, false, "操作1" };
std::cout << "第一次 co_await 后续执行,结果: " << result1 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第二次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result2 = co_await AsyncOperation{ 200, false, "操作2" };
std::cout << "第二次 co_await 后续执行,结果: " << result2 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第三次 co_await(就绪状态) ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result3 = co_await AsyncOperation{ 300, true, "操作3" };
std::cout << "第三次 co_await 后续执行,结果: " << result3 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "=== 协程函数执行完成 ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
int main() {
std::cout << "主函数开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
{
std::cout << "\n******** 创建协程任务 ********" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
auto task = detailed_async_example();
std::cout << "协程任务已创建,控制权返回主函数" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 给异步操作时间完成
std::cout << "\n主线程等待3秒..." << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "主线程等待结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
} // task 析构
std::cout << "\n主函数结束 " << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return 0;
}
接下来我们结合可运行代码样例实操讲解co_await与 Awaitable 对象。之前讲生成器时挂起逻辑是编译器隐式自动完成的,我们看不到完整的co_await执行链路;本次样例会显式手写自定义 Awaitable 类型,同时演示主线程手动恢复协程、后续拓展多异步线程恢复协程的底层逻辑。
一、样例整体结构说明
整套代码分为三大核心模块,分工清晰:
-
自定义
promise_type:协程帧内部存储、管控协程生命周期,无需实现yield_value(本样例不是生成器,没有co_yield); -
Task包装类:协程对外返回对象,内部持有协程句柄,外部依靠它操作、恢复、销毁协程; -
自定义 Awaitable 类型:核心演示对象,完整实现
await_ready、await_suspend、await_resume三接口,用来手动控制协程暂停、恢复、返回结果。
1. promise_type 细节拆解
-
内部多余存储值可以直接删除,本样例无数据产出,不需要缓存数值;
-
get_return_object():编译器自动调用,通过协程句柄构造Task对象返回,逻辑和之前生成器完全一致; -
initial_suspend()返回suspend_never:协程创建完成不立刻挂起,直接进入协程函数内部执行代码; -
final_suspend()返回suspend_always:协程全部逻辑执行完毕后再次挂起,协程帧不会自动销毁,必须手动调用句柄destroy()释放; -
仅实现
return_void():协程无co_return 变量,只需要无返回值收尾接口,不需要return_value; -
无
yield_value:全程不使用co_yield,编译器不会调用该接口,无需定义。
2. Task 包装类作用
Task 是协程对外暴露的载体,不能混淆两个 “返回” 概念:
它不是co_return的业务返回值,只是协程的操作句柄包装容器;
内部持有std::coroutine_handle<promise_type>,外部可以依靠句柄调用resume()恢复协程、destroy()释放协程帧;
构造函数由get_return_object()隐式调用,开发者不需要手动实例化;
析构函数:判断句柄是否有效,若协程帧未销毁,自动执行handle.destroy()释放内存,防止内存泄漏。
二、自定义 Awaitable 类型基础设计
我们手写简易 Awaitable 类,用来打印执行日志,直观展示三个核心函数的调用顺序,同时支持传递返回值给co_await表达式:
-
成员变量:
bool ready(默认 false,默认需要挂起)、标识名称、存储返回结果的变量; -
await_ready():返回内部 ready 标识,false 代表未就绪,需要挂起协程; -
await_suspend():打印日志,接收通用无类型协程句柄,这里基础样例仅打印流程,异步版本会在此注册 IO / 线程回调; -
await_resume():返回预设结果,该返回值就是co_await 实例整条表达式的运算结果;
所有打印日志仅用于调试观察执行顺序,业务代码中可以删除。
三、完整执行流程分步梳理(基础单线程版本)
步骤 1:调用协程函数,协程初始化
main 函数调用协程函数,编译器自动执行底层初始化流程:
-
在堆上分配协程帧,帧内构造
promise_type对象; -
调用
promise.get_return_object(),生成携带协程句柄的 Task 对象; -
调用
promise.initial_suspend(),返回suspend_never,底层等价co_await suspend_never; -
suspend_never的await_ready()返回 true,跳过挂起流程,协程不会暂停,直接进入协程函数内部代码。
步骤 2:执行 co_await 表达式,触发 Awaitable 完整流程
协程内部走到co_await 自定义Awaitable{},编译器自动转换标准执行逻辑:
-
构造 Awaitable 临时对象;
-
调用
await_ready(),返回 false,进入挂起分支; -
调用
await_suspend(协程句柄),打印日志; -
执行权交出,协程正式挂起,跳转回到 main 函数调用协程的位置,Task 对象返回给局部变量接收。
此时协程停留在co_await挂起点,主线程继续向下执行,如果不手动调用 resume,协程后续代码永远不会执行。
步骤 3:主线程调用 resume () 恢复协程
main 函数拿到 Task 对象,调用内部句柄resume()唤醒协程:
-
从协程帧恢复局部变量、程序计数器、寄存器上下文;
-
从上次挂起点继续执行,第一时间调用
await_resume(); -
await_resume返回预设数值(示例中 100),赋值给co_await左侧接收变量; -
打印获取到的返回值,协程函数剩余代码顺序执行;
-
协程函数走到末尾,编译器隐式插入
co_return;,调用promise.return_void()。
步骤 4:协程收尾 final_suspend 再次挂起
return_void()执行完成后,编译器执行co_await promise.final_suspend():
-
final_suspend返回suspend_always,await_ready返回 false; -
执行
await_suspend后协程再次挂起; -
执行权交还给 main 函数中
resume()调用处,主线程继续向下运行。
步骤 5:局部 Task 对象析构,销毁协程帧
Task 是 main 函数局部对象,离开作用域触发析构:
-
析构函数检测内部句柄有效;
-
调用
handle.destroy()释放堆上协程帧,销毁 promise、所有协程局部变量; -
协程生命周期彻底结束,未执行的代码不会再运行。
四、修改 initial_suspend 两种返回值带来的流程差异
场景 1:initial_suspend 返回 suspend_always
-
协程初始化完成后直接挂起,Task 对象先返回给 main 函数;
-
此时协程函数内部代码完全没有执行;
-
必须手动调用
resume(),才会进入协程内部,执行到co_await再次挂起。
场景 2:initial_suspend 返回 suspend_never(当前样例默认)
协程创建后直接运行内部代码,直到第一个co_await挂起点才暂停,提前执行一部分业务逻辑。
五、final_suspend 返回值的踩坑点说明
返回suspend_always(本样例) 协程执行完毕后挂起,协程帧不会自动释放,必须由 Task 析构函数手动调用destroy()销毁; 优势:协程结束后仍可通过句柄读取 promise 内部存储的返回值、异常;
返回suspend_never 协程执行完毕、走完return_void后,直接自动销毁堆上协程帧; 风险:后续代码如果再次操作句柄(调用 done ()、promise ()、destroy ()),会访问野指针,程序崩溃; 适用场景:协程结束后不再访问任何协程数据,也不会二次销毁句柄。
补充:如果同时设置final_suspend=suspend_never,并且 Task 析构依旧执行destroy(),会出现重复销毁协程帧,直接触发程序异常退出。
C++ 协程异步线程恢复复杂样
刚才我们学习的是 Awaitable、co_await 的极简入门样例,主要帮助大家看懂 co_await 的完整执行流程。接下来我们来看一个更复杂、更贴近工程框架的增强版样例,这个样例非常关键,是我们后续学习协程框架、异步 IO、线程池调度的核心铺垫。
首先跟大家说一下学习定位:我们现在手写协程底层、研究 co_await、Awaitable 原理,不是为了直接在业务项目手写裸协程,而是为了看懂市面上成熟的协程框架底层逻辑。
就像我们自己手写过生成器之后,再看别人封装好的生成器代码,就一目了然、完全不晦涩。同理,弄懂今天的复杂样例,以后看任何开源协程框架、异步调度框架,你都能看懂它底层是怎么挂起、怎么恢复、怎么调度线程的。
本次增强样例的核心亮点:实现跨线程恢复协程。 我们之前的极简样例,逻辑非常死板:主线程调用协程、协程挂起、依旧由主线程手动 resume 恢复协程。
但本次复杂样例完全不同: 主线程触发协程执行,协程遇到 co_await 主动挂起,不再由主线程恢复,而是新开一个异步子线程,由子线程来恢复协程、继续执行协程后续逻辑。
这个机制就是工业级协程 + 线程池 + IO 多路复用的核心雏形。
我给大家通俗讲一下工程真实用法: 我们的服务端会有一个主线程,专门负责 epoll/poll IO 多路复用监听,不停监听所有套接字的读写事件、新连接事件。
当某个套接字没有数据、没有新连接时,对应的读写协程就会 co_await 挂起,同时在 await_suspend 中注册 IO 监听事件,然后让出执行权。
主线程只负责监听事件,绝对不负责执行耗时的读写业务逻辑。一旦 epoll 监听到事件就绪,主线程会把「待恢复的协程句柄」包装成任务,抛给线程池,由线程池里的空闲线程来 resume 恢复协程,执行后续的读数据、业务处理逻辑。
这样做的优势极大:主线程永远只做监听,不会被业务阻塞,吞吐量拉满,所有业务逻辑由线程池并发执行。
这就是我们这个复杂样例要模拟的核心思想。
样例核心改动与参数说明
整套代码的基础结构、promise_type、Task 封装类,和之前极简样例完全一致,只有两处核心改动:
-
本次样例连续调用了三次
co_await,三次自定义Awaitable异步操作; -
三次操作参数不同:前两次传入
ready = false,代表必须挂起协程;第三次传入ready = true,await_ready返回true,无需挂起,直接执行; -
所有异步恢复逻辑,全部在
await_suspend内部创建分离式后台线程完成,实现跨线程恢复。
跨线程恢复核心执行逻辑详解
我们结合日志、线程 ID、执行流一步步拆解,这也是为什么代码中要打印线程 ID 的原因:直观区分每一段协程逻辑是哪个线程在执行。
-
程序启动,主线程调用协程函数,
promise_type的initial_suspend返回suspend_never,协程创建后不挂起,直接进入函数内部执行,全程由主线程执行初始化逻辑。 -
走到第一次
co_await:构造自定义AsyncOperation对象,ready=false,await_ready返回 false,进入挂起流程。 -
在
await_suspend内部,主线程创建新的异步后台线程,并调用detach()分离线程,变成守护线程,不受主线程生命周期直接管控。 -
线程创建完成后,协程正式挂起,执行权交还给主线程。
-
主线程返回主函数,进入
sleep_for(3s)休眠,目的是留出时间让异步线程执行,防止主线程直接退出、销毁协程对象,导致异步逻辑无法执行。
此时程序存在两个执行流:休眠的主线程、后台运行的异步线程。
异步线程休眠 1 秒模拟 IO 等待、异步任务完成后,主动调用 handle.resume() 跨线程恢复协程。 这里是本节课最核心的知识点: 哪个线程调用 resume(),哪个线程就接管协程后续所有执行逻辑。
原本协程挂起在主线程的执行流中,一旦被异步线程 resume,协程后续代码全部由异步子线程执行,和主线程彻底无关。
协程恢复后,优先执行 await_resume(),返回预设值 100,赋值给变量,继续向下执行代码。
随后进入第二次 co_await,依旧是 ready=false,再次触发挂起逻辑: 当前执行为上一次的异步线程,再次进入 await_suspend,创建第三个全新异步线程,随后当前异步线程触发挂起、返回调用点、线程结束销毁。
新创建的第三个线程,等待任务完成后,再次 resume 恢复协程,继续执行后续逻辑。
随后进入第三次 co_await,本次 ready=true,await_ready 返回 true,直接跳过挂起流程,无需创建线程、无需暂停,直接执行 await_resume,返回 300,协程逻辑一次性全部执行完毕,协程正常结束。
所有异步线程执行完任务后自动结束,主线程休眠三秒结束后,退出作用域,Task 对象析构,自动销毁协程句柄与协程帧,程序结束。
协程跨线程执行的核心特性总结
从整套执行流可以得出一个至关重要的结论: 协程不绑定固定线程! 协程挂起前在哪个线程执行,完全不影响恢复后的执行线程。 只要拿到协程句柄 coroutine_handle,任意线程都可以 resume 恢复协程,接管后续所有代码执行。
这就是协程搭配线程池的核心原理: IO 事件就绪后,线程池随便取出一个空闲线程,恢复协程、执行业务逻辑,实现高效并发调度。
协程跨线程调度的工程坑点
这个机制看似完美,但工程使用中有一个非常棘手的问题:线程锁问题。
普通线程锁,是线程独占的:加锁、解锁必须在同一个线程完成。
但协程是跨线程执行的: 你可能在 A 线程加锁、协程挂起,后续被 B 线程恢复、执行解锁逻辑。 加锁线程和解锁线程不是同一个线程,会直接导致锁机制错乱、程序崩溃、死锁、内存异常。
所以,原生 C++ 协程无法直接搭配普通互斥锁,协程框架必须配套实现跨线程协程锁,专门适配协程跨线程调度、挂起恢复的特性,这也是成熟协程框架的必备底层组件。
C++20 协程 co_await 进阶特性:对称转移、运算符重载、await_transform
同学们,现在我们已经对co_await后跟随Awaitable对象的基础逻辑有完整认知了:co_await expr会把表达式转为 Awaitable 对象,依次调用await_ready、await_suspend、await_resume三个核心函数。 之前讲到await_suspend存在多种合法返回值,其中返回协程句柄对应的对称转移机制,我们今天详细拆解,同时补充两类转换语法:重载operator co_await、Promise 内await_transform转换规则。
一、await_suspend 返回协程句柄:对称转移(链式协程调度)
1. 什么是默认行为(返回 void)
正常情况下await_suspend返回void,执行逻辑是: 我在主线程调用当前协程,协程执行到co_await触发挂起,执行权原路返还给调用当前协程的上层代码。 举个例子:main 调用协程 A,A 挂起后,执行权直接回到 main 函数,这是最基础的同步执行流程。
2. 对称转移(返回 std::coroutine_handle<>)
除了返回 void、bool,await_suspend还可以返回任意类型协程句柄,这就是对称转移,属于协程调度器底层核心设计:
行为变化:当前协程挂起后,执行权不返回上层调用方,直接切换到返回值指定的目标协程,线程执行流无缝跳转至另一个协程运行;
适用场景:线程池、自定义任务调度器。调度器内单个工作线程持续循环执行协程任务,一个协程挂起后,直接切换到队列中下一个待执行协程,不用切回调度循环,减少上下文切换开销;
实现思路:
在当前协程的promise_type内部新增一个协程句柄成员,命名next_handle,用来存储挂起后需要跳转执行的目标协程;
自定义 Awaitable 的await_suspend使用模板参数,接收带完整 Promise 类型的强类型句柄,而非无类型空句柄std::coroutine_handle<void>;
通过句柄拿到当前协程的 Promise 对象,读取预存的next_handle;
判断句柄是否为空:
-
空句柄:返回系统默认空协程句柄
noop_coroutine_handle,执行权回到原始调用方; -
有效句柄:直接返回目标协程句柄,触发对称转移,线程直接执行下一个协程。
3. 系统空协程 noop_coroutine_handle 作用
标准库提供noop_coroutine(),返回一个内置空协程句柄,这个内置协程没有任何业务逻辑,是一个什么都不做的占位协程:
-
该空协程
initial_suspend返回suspend_never,创建后直接执行完毕,不会挂起; -
语法规则:
await_suspend如果返回noop_coroutine_handle,编译器判定为不进行协程跳转,执行权原路返还给调用当前协程的上层代码,等价于返回 void 的默认行为; -
设计目的:统一
await_suspend返回值类型,无论跳转其他协程还是原路返回,都统一使用协程句柄作为返回类型,简化调度器模板代码。
4. 对称转移整体逻辑总结
await_suspend有两套执行分支:
返回noop_coroutine_handle:协程挂起,执行权回到调用本协程的上层代码;
返回自定义有效协程句柄:执行权直接切换到目标协程,实现链式无回调调度,多用于高性能异步调度框架。 这块机制了解底层原理即可,日常业务开发极少手写,主要是开源协程调度器的底层实现手段。
二、进阶语法 1:重载 operator co_await 类型转换
1. 基础限制
co_await关键字后面必须直接跟随合法 Awaitable 对象,如果一个自定义类型(比如示例中的AdvancedTask)没有实现await_ready/await_suspend/await_resume三接口,它本身不是 Awaitable,直接写co_await AdvancedTask{}会编译报错。
2. 重载 operator co_await 解决转换问题
我们可以给非 Awaitable 自定义类型重载operator co_await运算符,把自身类型转换为合法 Awaitable 对象,分两种实现方式:
-
成员函数重载:写在自定义类内部,隐式接收
this当前对象,内部构造一个包装了自身引用的 Awaitable 临时对象并返回; -
全局自由函数重载:写在类外部,接收自定义类型引用作为参数,同样返回 Awaitable 对象。
3. 左值 / 右值 /const 三重重载区分
operator co_await支持三种重载版本,匹配不同值类别对象,编译器自动匹配对应版本:
-
普通左值引用
operator co_await() &:匹配普通可修改对象; -
const 左值引用
operator co_await() const &:匹配 const 只读对象; -
右值引用
operator co_await() &&:匹配临时匿名对象、std::move 转移后的对象。
工程开发中不用强制实现三个版本,按需写一个即可;如果经常使用临时匿名任务对象,优先实现右值版本。
4. 底层本质
重载运算符只是语法糖:编译器遇到co_await 非Awaitable类型实例时,自动调用operator co_await,拿到转换后的标准 Awaitable 对象,再正常执行await_ready、await_suspend、await_resume整套流程,最终底层依旧依赖 Awaitable 三接口。
三、进阶语法 2:Promise 内部 await_transform 自动转换
1. 适用场景
第二种转换方案:不需要修改外部自定义类型,直接在协程的promise_type内部定义可选成员函数await_transform,用来把co_await后的任意表达式值,自动转为合法 Awaitable 对象。 举例:直接写co_await 123;,字面量 int 本身不是 Awaitable,编译器不会报错,会自动调用promise.await_transform(123),由该函数返回 Awaitable 对象。
2. 执行流程
代码co_await expr,expr 本身不是合法 Awaitable;
编译器优先查找当前协程 Promise 内是否存在await_transform成员函数;
将 expr 作为参数传入await_transform;
函数返回标准 Awaitable 对象,再执行完整挂起恢复流程。
四、两类转换机制对比总结
co_await语法要求其后表达式最终必须是 Awaitable 对象,非 Awaitable 类型有两种合法转换路径,二选一即可:
类型侧转换:重载 operator co_await 作用范围:只针对该自定义类型本身生效,任何地方使用该类型配合co_await都会触发转换; 适用场景:通用任务、IO 封装类,多处代码会 await 该类型。
协程 Promise 侧转换:await_transform 作用范围:仅当前协程生效,仅在该 Promise 对应的协程函数内触发转换; 适用场景:仅单个协程需要特殊转换规则,比如协程内直接 await 数字、字符串等基础类型。
两种转换本质完全一致,最终都会产出标准 Awaitable 对象,只是转换触发的位置、生效范围不同,根据业务场景选择即可。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)