跟我学c++中级篇——再谈C++20中的协程

一、协程

在前面分析过协程是什么,也对c++20中的协程的应用进行了举例说明。所以这次重点分析一下c++20中的整体构成及应用的方式。等明白了协程是如何动作的,各种情况下如下何处理相关的事件,那么在以后写协程时就不会死搬硬套了。

二、整体说明

在C++20中的协程中,首先要明白几个主要的类型和关键字。在协程中可以划分为三种类型以及一种状态,这三种类型(或者说表现出来的数据结构)是:
1、promise(promise_type)
这个在C++11中就有,此处和其意义相似,协程内部通过此对象来获取结果及异常。协程中要求返回对象必须是promis_type,这是一个接口类型。它一般要实现:

get_return_object: 获取协程的返回对象
initial_suspend:协程初始化的时挂起
final_suspend:协程结束时挂起的操作

initial_suspend和final_suspend 有点象钩子函数,也有点像JAVA的Spring中的拦截器,前者在执行协程(函数)前运行,后者在协程返回前执行。这里需要注意的是,如果final_suspend 运行了协程挂起的动作(比如使用了std::suspend_always{}),就需要记得在协程结束后手动destroy来销毁,否则的话,协程将自动销毁。这个和线程的detach和join有点类似。
另外它还有其它一些方法:

void return_void(): 如果没有实现co_return或单纯调用co_return以及co_return expr中expr返回值为void,必须有,否则报错
void return_value():co_return expr中expr返回值为非void
void yield_value():如果程序中调用了co_yield,则必须有
unhandled_exception(): 异常控制

同时,promise_type还会管理协程的相关栈空间,即一些临时变量、参数、返回值及一些寄存器的上下文等。

2、awaitable(awaiter)
awaitable和awaiter是比较让初学者有些感到迷惑的,有人可能觉得为什么要有两个类似的对象。其实这个非常好理解,前者是用来处理co_wait的expr的对象,后者是具体的处理对象或者等待器。而awaitable可以理解为居中的一层抽象虽然co_wait awaiter,但直接使用awaiter便不再依赖于抽象而是依赖于具体实现。所以便把awaiter抽象出来,具体由awaitable来实现。所以expr类型需要是一个awaitable类型。而awaitable可以转换成awaiter。
它其中需要实现的接口包括:

await_ready():是否挂起协程。返回 true,co_await不挂起函数,否则挂起(一般异步都是false)并调用await_suspend
await_resume():co_await expt的返回值,一般为空(如果有返回值则 auto ret = co_await expt)
await_suspend(handle):协程挂起时的行为,可能通过handle得到当前协程的状态,以此来处理挂起时的动作

在c++20的协程说明中有详细的说明哪几种情况可以转换成awaitable。如果没有什么复杂的动作,可以使用在标准库里实现的两个此类型的结构体:

struct suspend_never {_NODISCARD constexpr bool await_ready() const noexcept {return true;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};

代码很简单,注意noexcept,必须有。
这里重点需要说明一下await_suspend,其实异步处理最主要的就是在此处,它可以通过传入的外部包裹的协程handle来管理协程动作。所以在网上或者资料上的一些例程在此处直接调用resume,其实是没有展现出异步的作用。
另外在await_suspend中的参数如果有操作promise_type则可以写成std::coroutine_handle<>,这就是默认(void)或自动推导就可以了。

3、coroutine_handle
这个就是刚刚提到的std::coroutine_handle<>,它一般常用的有两个方法:

handle.resume():协程恢复执行
handle.destroy():销毁协程(在前文提到的final_suspend挂起时)

当然还done,from_address等接口,大家可以参考相关资料文档。
需要注意的是,这个句柄类似于浅拷贝,析构函数不处理相关的协程内部状态的内存,需要操作destroy函数,但大多数情况下都是自动销毁,只有在前面提到的那种情况下,需要手动调用destroy函数。同样,销毁掉句柄后,被复制的句柄成为了类似于悬垂指针的存在。

而一种状态是协程状态coroutine state,它主要有包含以下几点:
1、the promise object
2、the parameters (all copied by value)
3、some representation of the current suspension point, so that a resume knows where to continue, and a destroy knows what local variables were in scope(这个有点类似于线程上下文的意思)
4、local variables and temporaries whose lifetime spans the current suspension point.
协程的状态有点类似于线程的上下文,这个需要大家自己去体会。

在协程中主要有三个关键字co_wait,co_yield和co_return:

1、co_wait
它是一个一元运算符,用来处理协程的暂停并将控制权返回给调用者直到重新恢复协程,其操作的是一个表达式,即co_wati expr(即awaiter) 。和上面的awaitable对应。
2、co_yield
这个相当于暂停协程并返回一个值,而这个值可以被重新利用即前面提到的yield_value函数。
3、co_return
这个好理解,它直接就返回值并结束协程。

这样把三种类型的概念和协程的状态以及三个关键字的关系搞清楚,是不是协程的用法已经呼之欲出,小的细节可以在实际应用中踩踩坑并多看文档来处理。

三、协程的执行流程

看完上面的分析,下面再把它们的关系用流程串边一下,即协程启动后,它们之间如何配合工作的:
1、首先分配内存并初始化协程状态
2、相关参数复制到协程
3、构造promise对象
4、通过调用 promise.get_return_object()并保存结果供协程首次挂起返回值给调用者
5、执行co_await promise.initial_suspend(),如果无复杂需求可使用STL中定义的 std::suspend_always (始终挂起)和std::suspend_never(永不挂起)
6.1 协程函数执行至 co_return expr语句:
若 expr 为 void 则执行 promise.return_void(),否则执行 promise.return_value(expr)
按照创建顺序的倒序销毁局部变量和临时变量
执行 co_await promise.final_suspend()

6.2 当协程执行到 co_yield expr语句:
调用co_await promise.yield_value(expr)

6.3 当协程执行到 co_await expr语句:
通过 expr 获得 awaiter 对象。
执行 awaiter.await_ready(),若为 true 则直接返回awaiter.await_resume(),否则将协程挂起并保存状态,执行 awaiter.await_suspend(),若其返回值为 void 或者 true 则成功挂起,将控制权返还给调用者,直到 handle.resume() 执行后该协程才会恢复执行,将 awaiter.await_resume() 作为表达式的返回值

6.4 当协程因为某个未捕获的异常导致终止:
捕获异常并调用 promise.unhandled_exception()
调用 co_await promise.final_suspend() 并 co_await 它的结果(例如,恢复某个继续或发布某个结果)。此时开始恢复协程是未定义行为。

6.5 当协程状态销毁时(通过协程句柄主动销毁 / co_return 返回 / 未捕获异常):
调用promise 对象的析构函数
调用传入参数的析构函数
释放协程状态占用内存
转移执行回调用方/恢复方

以上这些在官方文档中有更详细的说明,大家可以在实际开发中去查阅相关信息。
是不是有点复杂,是有点复杂啊。这个样子还是不能让普通程序员用得自由,也就是没有解决简单的问题,所以协程还需要继续努力(网上有各种基于此的协程库)。

四、例程

这里的例程重点是对上面的分析的一种代码的说明,请仔细看代码并和上面的分析说明对照,会有更深的理解,先看一个例子:

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>auto switch_to_new_thread(std::jthread& out)
{struct awaitable{std::jthread* p_out;bool await_ready() { return false; }void await_suspend(std::coroutine_handle<> h){std::jthread& out = *p_out;if (out.joinable())throw std::runtime_error("jthread 输出参数非空");out = std::jthread([h] { h.resume(); });// 潜在的未定义行为:访问潜在被销毁的 *this// std::cout << "新线程 ID:" << p_out->get_id() << '\n';std::cout << "新线程 ID:" << out.get_id() << '\n'; // 这样没问题}void await_resume() {}};return awaitable{&out};
}struct task
{struct promise_type{task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {} //和上文中的说明对照void unhandled_exception() {}};
};task resuming_on_new_thread(std::jthread& out)
{std::cout << "协程开始,线程 ID:" << std::this_thread::get_id() << '\n';co_await switch_to_new_thread(out);// 等待器在此销毁std::cout << "协程恢复,线程 ID:" << std::this_thread::get_id() << '\n';
}int main()
{std::jthread out;resuming_on_new_thread(out);
}

可能的输出:

协程开始,线程 ID:139972277602112
新线程 ID:139972267284224
协程恢复,线程 ID:139972267284224

怎么样,协程进行了线程的自动调度,方便很多吧。但是这种在不同线程间调度时需要注意,不能在另外一个线程内恢复协程否则容易出现数据处理的问题,这个就看应用的场景了。用不好,就直接挂了。这个例子中也有风险提示,大家看一下就明白了。
再看一个简单的例子:

#include <coroutine>
#include <iostream>struct MyCoroutine {struct MyPromise {MyCoroutine get_return_object() {return std::coroutine_handle<MyPromise>::from_promise(*this);}std::suspend_never initial_suspend() { return {}; }// 使用suspend_always,需要手动 destroy,noexcept必须auto final_suspend() noexcept{ return   std::suspend_always{}; }void unhandled_exception() {}void return_void() {} //返回空必须有};using promise_type = MyPromise;//尖括号中MyPromise类型可自动推导MyCoroutine(std::coroutine_handle<> h) : handle(h) {}std::coroutine_handle<MyPromise> handle;};MyCoroutine first() {std::cout << "this is my  first \n" ;co_await std::suspend_always{};std::cout << "coroutine!\n";
}int main() {MyCoroutine my = first();my.handle.resume();my.handle.destroy();//手动释放return 0;
}

看到上面的第一个例子中的风险,那么如何安全的调度协程实现await_suspend的异步操作情况,两种方法,一种是在主线程中写一个任务处理函数,不断的去探查协程挂起后的操作,并把await_suspend的恢复放到主线程中去。第二种方法就是使用和线程池类似的方式,将任务注册到队列,通过消息来驱动协程工作并最终将任务结果返回。
github上有阿里开源的协程库,应用起来会更简单(https://github.com/alibaba/async_simple)。
源码面前,了无秘密。

五、总结

协程最大特点是它可以跨越线程来进行操作,而在线程中一般数据处理要么在线程中独自控制要么需要加锁。所以协程应用起来更灵活,这也是为什么协程能更好的发挥线程的作用并同时呈现更好异步操作的原因。这个在GO的协程测试中已经有验证,协程可以开出几百万个,但线程一般到几百个就达到瓶颈了。
协程的开发会在大多数场景下替代线程的开发,只有一个最大原因,简单,功能强大倒是其次。希望c++中的协程在后面会变得更简单好用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/588524.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分布式存储考点梳理 + 高频面试题

欢迎来到分布式存储模环节&#xff0c;本文我将和你一起梳理面试中分布式系统的数据库的高频考点&#xff0c;做到温故知新。 面试中如何考察分布式存储 广义的分布式存储根据不同的应用领域&#xff0c;划分为以下的类别&#xff1a; 分布式协同系统 分布式文件系统 分布式…

Xline command 去重机制(一)—— RIFL 介绍

为什么要对 command 去重&#xff1f; 在一个接收外部 command 的系统中&#xff0c;通常一个 command 至少要执行一次&#xff0c;我们称其为 at-least-once semantics。如果一个 command 执行失败&#xff0c;系统内部经常会实现一套重试结构来尝试恢复这个问题&#xff0c;…

HTML 基础

文章目录 01-标签语法标签结构 03-HTML骨架04-标签的关系05-注释06-标题标签07-段落标签08-换行和水平线09-文本格式化标签10-图像标签图像属性 11-路径相对路径绝对路径 12-超链接标签13-音频14-视频 01-标签语法 HTML 超文本标记语言——HyperText Markup Language。 超文本…

【分布式配置中心】聊聊Apollo的安装与具体配置变更的原理

【管理设计篇】聊聊分布式配置中心 之前就写过一篇文章&#xff0c;介绍配置中心&#xff0c;但是也只是简单描述了下配置中心的设计点。本篇从apollo的安装到部署架构到核心原理进一步解读&#xff0c;大概看了下apollo的原理&#xff0c;感觉没有必要深究&#xff0c;所以就…

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项样题卷③

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 目录 2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 模块…

2024黑龙江省职业院校技能大赛暨国赛选拔赛应用软件系统开发赛项(高职组)赛题第1套

2024黑龙江省职业院校技能大赛暨国赛选拔赛 应用软件系统开发赛项&#xff08;高职组&#xff09; 赛题第1套 竞赛说明 目录 需要竞赛源码资料可私信博主&#xff01; 竞赛说明 模块一&#xff1a;系统需求分析 任务1&#xff1a;制造执行MES—质量管理—质检标准&#xff…

FreeRTOS学习--49讲 事件标志位

事件标志位&#xff1a; 用一个bit位来表示事件是否发生&#xff0c;只有0(未发生)和1(已发生)两种状态 事件组&#xff1a;事件组是一组事件标志的集合&#xff0c;一组事件组包含了EventBites_t数据类型的变量&#xff0c;该变量高8未不能作为事件标志&#xff0c;用于存储控…

以一种访问权限不允许的方式做了一个访问套接字的尝试

python -m http.server 启动失败 原因是端口被占用&#xff0c;但是使用 netstat -ano|findstr 8000 却没发现占用&#xff0c;最后发现是hyper-v占用了&#xff0c;要给容器使用。 使用命令 netsh int ipv4 show dynamicport tcp可以查看TCP 动态端口范围的情况。 netsh int…

人机对话--关于意识机器

人机对话——关于意识机器 这段内容是我和《通义千问》的对话。这本身展示的是人工智能的效果&#xff0c;同时这里面的内容也有人工智能相关&#xff0c;与各位分享。 我&#xff1a;阿尼尔赛斯 《意识机器》这本书写的是什么&#xff1f; 通义千问&#xff1a; 阿尼尔赛斯…

❀My排序算法学习之冒泡排序❀

目录 冒泡排序(Bubble Sort):) 一、定义 二、算法原理 三、算法分析 时间复杂度 算法稳定性 算法描述 C语言 C++ 算法比较 插入排序 选择排序 快速排序 归并排序 冒泡排序(Bubble Sort):) 一、定义 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单…

【北亚数据恢复】mysql表被truncate,表数据被delete的数据恢复案例

云服务器数据恢复环境&#xff1a; 华为ECS云服务器&#xff0c;linux操作系统&#xff0c;mysql数据库&#xff08;innodb引擎&#xff09;。作为网站服务器使用。 云服务器故障&#xff1a; 在执行mysql数据库版本更新测试时&#xff0c;误将本应该在测试库上执行的sql脚本执…

渗透测试计划模板

引言 1.1 目的 1.2 范围 1.3 参考文档 1.4 术语和定义 系统概述 2.1 系统描述 2.2 系统架构 2.3 技术栈 2.4 相关方 目标与风险评估 3.1 目标设定 3.2 风险评估 3.3 风险优先级排序 测试方法与工具 4.1 黑盒测试方法 4.2 白盒测试方法 4.3 灰盒测试方法 4.4 常用渗透测试工具…

亚马逊云科技Amazon Q,一款基于生成式人工智能的新型助手

近日&#xff0c;亚马逊云科技宣布推出Amazon Q&#xff0c;这是一款基于生成式人工智能&#xff08;AI&#xff09;的新型助手&#xff0c;专为辅助工作而设计&#xff0c;可以根据您的业务量身定制。通过连接到公司的信息存储库、代码、数据和企业系统&#xff0c;可以使用Am…

个人游戏启动器 | 游戏数据库 playnite 折腾记录

环境&#xff1a;Windows 11 问题&#xff1a;使用平板串联PC游戏后&#xff0c;需要一个本地的PC启动器 解决办法&#xff1a;使用playnite搭配插件 背景&#xff1a;我是个单机游戏爱好者&#xff0c;因为某些原因&#xff0c;需要串流游玩&#xff0c;需要一个方便手柄操作的…

arkts状态管理使用(@State、@Prop、@Link、@Provide、@Consume、@objectLink和@observed)

一、状态管理 1.在声明式UI中&#xff0c;是以状态驱动视图更新&#xff1a; ①状态&#xff08;State&#xff09;:指驱动视图更新的数据&#xff08;被装饰器标记的变量&#xff09; ②视图&#xff08;View&#xff09;:基于UI描述渲染得到用户界面 注意&#xff1a; ①…

【零基础入门VUE】VueJS - 模板

✍面向读者&#xff1a;所有人 ✍所属专栏&#xff1a;零基础入门VUE专栏https://blog.csdn.net/arthas777/category_12537076.html 我们在前面的章节中学习了如何在屏幕上以文本内容的形式输出。在本章中&#xff0c;我们将学习如何在屏幕上以 HTML 模板的形式获取输出。 为了…

NLP常见问题

transformer中的attention为什么scaled?-CSDN博客 Transformer模型中的Feed-Forward层的作用_transformer feed-forward解释-CSDN博客 关于ChatGPT&#xff1a;GPT和BERT的差别&#xff08;易懂版&#xff09; Index - 算法工程师笔记 Transformer模型中&#xff0c;decod…

什么是AI PC,又有哪些产品

最近一段时间&#xff0c;AI PC成为一个流行词。Intel在发布Core Ultra处理器的时候&#xff0c;直接使用了AI PC这个词语&#xff0c;而各大厂商发布相应的笔记本产品时&#xff0c;也使用了AI Ready的宣传词。而在Intel之前&#xff0c;AMD在发布自己的新一代APU的时候&#…

Linux中安装了openjdk后jps command not found

一、问题场景 在Linux中用yum安装了openjdk-17&#xff0c;也在.bashrc中配置了环境变量JAVA_HOME以及bin目录的PATH。 但是在运行jps命令时依然报错找不到命令 二、原因分析 进入到$JAVA_HOME/bin目录查看&#xff0c;发现只有寥寥几个命令&#xff0c;压根没有jps命令&…

《现代操作系统》第十二章习题答案

计算机硬件的改进主要归功于更小的晶体管。一些限制因素包括&#xff1a;(a) 光的波动性可能限制传统光刻技术制造集成电路的能力&#xff0c;(b) 固体中个别原子的迁移性可能导致非常薄的半导体、绝缘体和导体层的性能退化&#xff0c;(c) 背景辐射活性可能破坏分子键或影响非…