当前位置: 首页 > news >正文

探究C++20协程(2)——取值、传值、销毁与序列生成器实现

序列生成器是一个非常经典的协程应用场景,尤其是在需要惰性生成数据或处理潜在无限的数据流时。

序列生成器概念:序列生成器允许程序按需生成序列中的下一个元素,而不是一次性计算整个序列。这种方式可以节省内存,并允许处理无限或未知长度的数据序列。

实现目标

简单的说,序列生成器通常的实现就是在一个协程内部通过某种方式向外部传一个值出去,并且将自己挂起,外部调用者则可以获取到这个值,并且在后续继续恢复执行序列生成器来获取下一个值。

显然,挂起和向外部传值的任务就需要通过 co_await 来完成了,外部获取值的任务就要通过协程的返回值来完成。

由此程序大致框架如下:

Generator sequence() {int i = 0;while (true) {co_await i++;}
}int main() {auto generator = sequence();for (int i = 0; i < 10; ++i) {std::cout << generator.next() << std::endl;}
}

在generator 有个 next 函数,调用它时需要想办法让协程恢复执行,并将下一个值传出来。

调用者获取值

generator 的类型就是我们即将实现的序列生成器类型 Generator,结合上一篇文章当中对于协程返回值类型的介绍,我们先大致给出它的定义:

struct Generator {struct promise_type {// 开始执行时直接挂起等待外部调用 resume 获取下一个值std::suspend_always initial_suspend() { return {}; };// 执行结束后不需要挂起std::suspend_never final_suspend() noexcept { return {}; }// 为了简单,我们认为序列生成器当中不会抛出异常,这里不做任何处理void unhandled_exception() { }// 构造协程的返回值类型Generator get_return_object() {return Generator{};}// 没有返回值void return_void() { }};int next() {//这里需要恢复线程}
};

想要在 Generator 当中 resume 协程的话,需要拿到 coroutine_handle。

promise_type 是连接协程内外的桥梁,标准库提供了一个通过 promise_type 的对象的地址获取 coroutine_handle 的函数,它实际上是 coroutine_handle 的一个静态函数:

//vs2022
struct coroutine_handle {constexpr coroutine_handle() noexcept = default;constexpr coroutine_handle(nullptr_t) noexcept {}_NODISCARD static coroutine_handle from_promise(_Promise& _Prom) noexcept { // strengthenedconst auto _Prom_ptr  = const_cast<void*>(static_cast<const volatile void*>(_STD addressof(_Prom)));const auto _Frame_ptr = __builtin_coro_promise(_Prom_ptr, 0, true);coroutine_handle _Result;_Result._Ptr = _Frame_ptr;return _Result;}

这样只需要在 get_return_object 函数调用时,先获取 coroutine_handle,然后再传给即将构造出来的 Generator 即可。

协程内部挂起并传值

观察一下最终实现的效果:

Generator sequence() {int i = 0;while (true) {co_await i++;}
}

特别需要注意的是 co_await i++; 其 后面的是一个整型值,而不是在前面的文章当中提到的满足等待体(awaiter)条件的类型,这种情况下该怎么办呢?

实际上,对于 co_await 表达式当中 expr 的处理,C++ 有一套完善的流程:

  • 如果 promise_type 当中定义了 await_transform 函数,那么先通过 promise.await_transform(expr) 来对 expr 做一次转换,得到的对象称为 awaitable;否则 awaitable 就是 expr 本身。

  • 接下来使用 awaitable 对象来获取等待体(awaiter)。如果 awaitable 对象有 operator co_await 运算符重载,那么等待体就是 operator co_await(awaitable),否则等待体就是 awaitable 对象本身。

那么只需要为数据类型实现一个 operator co_await 的运算符重载即可。

struct Generator {struct promise_type {int value;// 传值的同时要挂起,值存入 value 当中std::suspend_always await_transform(int value) {this->value = value;return {};}};std::coroutine_handle<promise_type> handle;int next() {handle.resume();// 外部调用者或者恢复者可以通过读取 valuereturn handle.promise().value;}
};

定义了 await_transform 函数之后,co_await expr 就相当于 co_await promise.await_transform(expr) 了。

协程的销毁

问题1:无法确定是否存在下一个元素

当外部调用者或者恢复者试图调用 next 来获取下一个元素的时候,它其实并不知道能不能真的得到一个结果。

为了解决这个问题,我们需要增加一个 has_next 函数,用来判断是否还有新的值传出来,has_next 函数调用的时候有两种情况:

  • 已经有一个值传出来了,还没有被外部消费
  • 还没有现成的值可以用,需要尝试恢复执行协程来看看还有没有下一个值传出来
struct Generator {bool has_next() {// 协程已经执行完成if (handle.done()) {return false;}// 协程还没有执行完成,并且下一个值还没有准备好if (!handle.promise().is_ready) {handle.resume();}if (handle.done()) {// 恢复执行之后协程执行完,这时候必然没有通过 co_await 传出值来return false;} else {return true;}}int next() {if (has_next()) {// 此时一定有值,is_ready 为 true // 消费当前的值,重置 is_ready 为 falsehandle.promise().is_ready = false;return handle.promise().value;}throw ExhaustedException();}
};

问题2:协程状态的销毁比 Generator 对象的销毁更早

协程的状态在协程体执行完之后就会销毁,除非协程挂起在 final_suspend 调用时。为了让协程的状态的生成周期与 Generator 一致(在Generator可能会使用导协程状态),我们必须将协程的销毁交给 Generator 来处理:

struct Generator {class ExhaustedException: std::exception { };struct promise_type {// 总是挂起,让 Generator 来销毁std::suspend_always final_suspend() noexcept { return {}; }};~Generator() {// 销毁协程handle.destroy();}
};

问题3:复制对象导致协程被销毁

在 Generator 的析构函数当中销毁协程,这本身没有什么问题。但如果把 Generator 对象做一下复制:

Generator returns_generator() {auto g = sequence();if (g.has_next()) {std::cout << g.next() << std::endl;}return g;
}

由于把 g 当做返回值返回了,这时候 g 这个对象就发生了一次复制,然后临时对象被销毁,协程也就没了,再调用直接dump。

为了解决这个问题,需要妥善地处理 Generator 的复制构造器:

struct Generator {explicit Generator(std::coroutine_handle<promise_type> handle) noexcept: handle(handle) {}Generator(Generator &&generator) noexcept: handle(std::exchange(generator.handle, {})) {}Generator(Generator &) = delete;Generator &operator=(Generator &) = delete;~Generator() {if (handle) handle.destroy();}
}

只提供了右值复制构造器,对于左值复制构造器,我们直接删除掉以禁止使用。原因也很简单,对于每一个协程实例,都有且仅能有一个 Generator 实例与之对应,因此我们只支持移动对象,而不支持复制对象。

序列生成器完整实现

#include <coroutine>
#include <exception>
#include <iostream>
#include <thread>struct Generator {class ExhaustedException : std::exception { };struct promise_type {int value;bool is_ready = false;std::suspend_always initial_suspend() { return {}; };std::suspend_always final_suspend() noexcept { return {}; }std::suspend_always await_transform(int value) {this->value = value;is_ready = true;return {};}void unhandled_exception() {}Generator get_return_object() {return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };}void return_void() { }};std::coroutine_handle<promise_type> handle;bool has_next() {if (handle.done()) {return false;}if (!handle.promise().is_ready) {handle.resume();//让协程恢复执行}if (handle.done()) {return false;}else {return true;}}int next() {if (has_next()) {handle.promise().is_ready = false;return handle.promise().value;}throw ExhaustedException();}explicit Generator(std::coroutine_handle<promise_type> handle) noexcept: handle(handle) {}Generator(Generator&& generator) noexcept: handle(std::exchange(generator.handle, {})) {}Generator(Generator&) = delete;Generator& operator=(Generator&) = delete;~Generator() {if (handle) handle.destroy();}
};Generator sequence() {int i = 0;while (i < 5) {co_await i++;}
}Generator returns_generator() {auto g = sequence();if (g.has_next()) {std::cout << g.next() << std::endl;}return g;
}int main() {auto generator = returns_generator();for (int i = 0; i < 15; ++i) {if (generator.has_next()) {std::cout << generator.next() << std::endl;}else {break;}}return 0;
}

使用 co_yield

C++ 当中的 co_yield expr 等价于 co_await promise.yield_value(expr),我们只需要将前面例子当中的 await_transform 函数替换成 yield_value 就可以使用 co_yield 来传值了:

std::suspend_always yield_value(int value) {this->value = value;is_ready = true;return {};}

通常情况下使用 co_await 更多的关注点在挂起自己,等待别人上,而使用 co_yield 则是挂起自己传值出去。

使用序列生成器生成斐波那契数列

Generator fibonacci() {co_await 0; // fib(0)co_await 1; // fib(1)int a = 0;int b = 1;while (true) {co_await (a + b); // fib(N), N > 1b = a + b;a = b - a;}
}int main() {auto generator = fibonacci();for (int i = 0; i < 15; ++i) {if (generator.has_next()) {std::cout << generator.next() << std::endl;}else {break;}}return 0;
}

fibonacci():通过连续的 co_await 表达式生成斐波那契数列的值。首先固定地生成 0 和 1,然后进入循环,不断计算后续数值并通过 co_await 暂停和恢复协程,以生成数列。

协程的启动和恢复是通过 Generator::has_next 和 Generator::next 中的 handle.resume() 来控制的。

每次 co_await 在 fibonacci 中被调用时,协程暂停,并在 await_transform 中处理新的值。

当 co_await 后的表达式执行完毕后,协程在 await_transform 返回的挂起点恢复。

相关文章:

探究C++20协程(2)——取值、传值、销毁与序列生成器实现

序列生成器是一个非常经典的协程应用场景,尤其是在需要惰性生成数据或处理潜在无限的数据流时。 序列生成器概念&#xff1a;序列生成器允许程序按需生成序列中的下一个元素&#xff0c;而不是一次性计算整个序列。这种方式可以节省内存&#xff0c;并允许处理无限或未知长度的…...

【前端面试3+1】12 toktn验证过程、面向对象特性、webpack和vite的区别、【字符串中的第一个唯一字符】

一、token验证过程 用户登录&#xff1a;用户提供用户名和密码进行登录。服务器验证&#xff1a;服务器接收到用户提供的用户名和密码&#xff0c;进行验证。生成token&#xff1a;如果用户名和密码验证通过&#xff0c;服务器会生成一个token&#xff0c;通常包含一些加密的信…...

机器人瓶胚检测工作站(H3U脉冲轴控制)

1、变量定义 2、程序监控1 2、 程序监控2 3、程序监控3 机器人输送料和机构的动作安全尤为重要&#xff0c;下面我们讨论下安全联锁控制逻辑 4、相机拍照触发信号 5、相机拍照触发时序...

数字货币:未来金融的崭新篇章

一、数字货币是什么&#xff1f; 数字货币是一种基于区块链技术的货币&#xff0c;它通过去中心化的方式发行和交易&#xff0c;无需传统的金融机构参与。数字货币的交易过程公开透明&#xff0c;可以确保交易的真实性和不可篡改性。比特币、以太坊、瑞波币等是目前比较知名的…...

USACO18DEC部分题 补题报告

一、Convention S P5119 [USACO18DEC] Convention S 题意 给定大巴的数量&#xff0c;容量&#xff0c;奶牛的数量和到来的时间&#xff0c;要求合理安排大巴的发车时间使奶牛的等待时间最小&#xff0c;求出奶牛最大等待时间的最小值 思路 本题使用二分&#xff0c;输入之…...

聊一聊一些关于npm、pnpm、yarn的事

前言 整理了最近的闲聊&#xff0c;话题是前端各个包管理器&#xff0c;如果分享的不对或者有异议的地方&#xff0c;麻烦请及时告诉我~ 耐心看完&#xff0c;也许你会有所收获~ 概述 本文阅读时间&#xff1a;10-15分钟左右&#xff1b; 难度&#xff1a;初级&#xff0c…...

c语言多功能计算软件170

定制魏&#xff1a;QTWZPW&#xff0c;获取更多源码等 目录 题目 要求 主要代码片段 题目 设计一个计算器软件&#xff0c;具备如下功能提示界面。 要求 设计出界面&#xff0c;注意界面名称最后为自己的姓名&#xff1b;&#xff08;20分&#xff09;能够实现加、减、乘、…...

python图形化展示数据:保存为图片后查看

python debug时需要图像化展示数据&#xff0c;有三种方法。 方法一&#xff1a;t是值在[0, 255]之间的numpy数组&#xff0c;形状为 [ x ∗ x ∗ 3 ] [x*x*3] [x∗x∗3]&#xff0c;其中3为channel数。&#xff08;使用t.permute(1,2,0)变换通道&#xff0c;使用np.squeeze(t…...

PostgreSQL入门到实战-第二十四弹

PostgreSQL入门到实战 PostgreSQL中表连接操作(八)官网地址PostgreSQL概述PostgreSQL中CROSS JOIN命令理论PostgreSQL中CROSS JOIN命令实战更新计划 PostgreSQL中表连接操作(八) 使用PostgreSQL CROSS JOIN从连接的表中生成行的笛卡尔乘积。 官网地址 声明: 由于操作系统, 版…...

Spring Boot 统一功能处理(二)

本篇主要介绍Spring Boot统一功能处理中的统一数据返回格式。 目录 一、定义统一的返回类 二、配置统一数据格式 三、测试配置效果 四、统一格式返回的优点 五、源码角度解析String问题 一、定义统一的返回类 在我们的接口在处理请求时&#xff0c;返回的结果可以说是参…...

Flutter开发基础之动画专题

Flutter开发基础之动画专题 动画设计的作用是让UI界面更流畅、直观&#xff0c;能够有效的提升用户体验。 在Flutter开发中&#xff0c;动画分为多个方面&#xff1a; 基础动画、页面交互动画、绘图动画、矩阵变换等。 基本动画 常用的基本动画有透明度动画、缩放动画、旋转动…...

PHP 图片裁剪类封装

PHP工具类 图片裁剪类封装 <?php namespace App\Utils;/*** 图片裁剪工具类* author 田小涛* date 2020年7月23日* comment**/ class ImageCropUtils {private $sImage;private $dImage;private $src_file;private $dst_file;private $src_width;private $src_height;priv…...

Android 14.0 SystemUI修改状态栏电池图标样式为横屏显示

1.概述 在14.0的系统rom产品定制化开发中,对于原生系统中SystemUId 状态栏的电池图标是竖着显示的,一般手机的电池图标都是横屏显示的 可以觉得样式挺不错的,所以由于产品开发要求电池图标横着显示和手机的样式一样,所以就得重新更换SystemUI状态栏的电池样式了 如图: 2.S…...

FPGA:图像数字细节增强算法(工程+仿真+实物,可用毕设)

目录 日常唠嗑一、视频效果二、硬件及功能1、硬件选择2、功能3、特点 未完、待续……四、工程设计五、板级验证六、工程获取 日常唠嗑 有2个多月没写文章了&#xff0c;又是老借口&#xff1a;“最近实在是很忙”&#x1f923;&#xff0c;不过说真&#xff0c;确实是比较忙&am…...

Android netty的使用

导入netty依赖 implementation io.netty:netty-all:4.1.107.Final使用netty 关闭netty /*** 关闭*/private void closeSocket() {LogUtils.i(TAG, "closeSocket");if (nettyManager ! null) {nettyManager.close();nettyManager null;}if (nettyExecutor ! null) {…...

苹果电脑启动磁盘是什么意思 苹果电脑磁盘清理软件 mac找不到启动磁盘 启动磁盘没有足够的空间来进行分区

当你一早打开苹果电脑&#xff0c;结果系统突然提示&#xff1a; “启动磁盘已满&#xff0c;需要删除部分文件”。你会怎么办&#xff1f;如果你认为单纯靠清理废纸篓或者删除大型文件就能释放你的启动磁盘上的空间&#xff0c;那就大错特错了。其实苹果启动磁盘的清理技巧有很…...

【Java SE】多态

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. 多态1.1 多态是什么1.2 多态的意义1.3 多态的实现条件 2. 重写2.1 重写的概念2.2 重写的规则2.3 重写与重…...

Yarn vs npm的大同小异Yarn是什么?

Yarn vs npm的大同小异&Yarn是什么&#xff1f; 一、Yarn、npm是什么&#xff1f;二、Yarn vs npm&#xff1a;特性差异总结 一、Yarn、npm是什么&#xff1f; npm是Node.js的包管理器&#xff0c;是由Chris Korda维护。 npm,它全称为Node Package Manager&#xff0c;是…...

1.Godot引擎|场景|节点|GDS|介绍

Godot介绍 Godot是一款游戏引擎 可以通过在steam商城免费下载 初学者和编程基础稍差的推荐学习使用GDScript&#xff0c;和python有些相似 Godot节点 Godot的开发思想——围绕节点 节点的特征与优势 最常用基本的开发组件大部分都具有具体的功能&#xff0c;如图片&#xf…...

springboot3 redis 实现分布式锁

分布式锁介绍 分布式锁是一种在分布式系统中用于控制不同节点上的进程或线程对共享资源进行互斥访问的技术机制。 在分布式环境中&#xff0c;多个服务可能同时访问和操作共享资源&#xff0c;如数据库、文件系统等。为了保持数据的一致性和完整性&#xff0c;需要确保在同一…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

简易版抽奖活动的设计技术方案

1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)

0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述&#xff0c;后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作&#xff0c;其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

给网站添加live2d看板娘

给网站添加live2d看板娘 参考文献&#xff1a; stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下&#xff0c;文章也主…...

水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关

在水泥厂的生产流程中&#xff0c;工业自动化网关起着至关重要的作用&#xff0c;尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关&#xff0c;为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多&#xff0c;其中不少设备采用Devicenet协议。Devicen…...

【堆垛策略】设计方法

堆垛策略的设计是积木堆叠系统的核心&#xff0c;直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法&#xff0c;涵盖基础规则、优化算法和容错机制&#xff1a; 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则&#xff1a; 大尺寸/重量积木在下&#xf…...

算术操作符与类型转换:从基础到精通

目录 前言&#xff1a;从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符&#xff1a;、-、*、/、% 赋值操作符&#xff1a;和复合赋值 单⽬操作符&#xff1a;、--、、- 前言&#xff1a;从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...

【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)

旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据&#xff01;该数据集源自2025年4月发表于《地理学报》的论文成果…...