探究C++20协程(1)——C++协程概览

什么是协程?

协程就是一段可以挂起(suspend)和恢复(resume)的程序,一般而言,就是一个支持挂起和恢复的函数。

一般情况下,函数一旦开始,就无法暂停。如果一个函数能够暂停,那它就可以被认为是我们开头提到的协程。所以挂起(suspend)就可以理解成暂停,恢复(resume)就理解成从暂停的地方继续执行。

Result Coroutine() {std::cout << 1 << std::endl;co_await std::suspend_always{};std::cout << 2 << std::endl;co_await std::suspend_always{};std::cout << 3 << std::endl;
};

Result 是按照协程的规则定义的类型,之后再详细介绍。在 C++ 20当中,一个函数的返回值类型如果是符合协程的规则的类型,那么这个函数就是一个协程。

co_await std::suspend_always{};,其中 co_await 是个关键字,会使得当前函数(协程)的执行被挂起。

在控制台看到输出 1 以后,很可能过了很久才看到 2,这个“很久”也一般不是因为当前执行的线程被阻塞了,而是当前函数(协程)执行的位置被存起来,在将来某个时间点又读取出来继续执行的。

协程的状态

对应线程,协程也有执行,挂起,恢复,异常,返回状态。

C++ 协程会在开始执行时的第一步就使用 operator new 来开辟一块内存来存放这些信息,这块内存或者说这个对象又被称为协程的状态(coroutine state)。

协程的状态不仅会被用于存放挂起时的位置(后称为挂起点),也会在协程开始执行时存入协程体的参数值。例如:

Result Coroutine(int start_value) {std::cout << start_value << std::endl;co_await std::suspend_always{};std::cout << start_value + 1 << std::endl;
};

这里的 start_value 就会被存入协程的状态当中。

需要注意的是,如果参数是值类型,他们的值或被移动或被复制(取决于类型自身的复制构造和移动构造的定义)到协程的状态当中;如果是引用、指针类型,那么存入协程的状态的值将会是引用或指针本身,而不是其指向的对象,这时候需要开发者自行保证协程在挂起后续恢复执行时参数引用或者指针指向的对象仍然存活。

与创建相对应,在协程执行完成或者被外部主动销毁之后,协程的状态也随之被销毁释放(编译器处理,不需要显式调用)。

协程的挂起

C++ 通过 co_await 表达式来处理协程的挂起,表达式的操作对象则为等待体(awaiter)

等待体需要实现三个函数,这三个函数在挂起和恢复时分别调用。

await_ready

标准库当中提供了两个非常简单直接的等待体,struct suspend_always 表示总是挂起,struct suspend_never 表示总是不挂起。这二者的功能主要就是依赖 await_ready 函数的返回值:

struct suspend_never {constexpr bool await_ready() const noexcept {return true;  // 返回 true,总是不挂起}//等待体其他内容...
};struct suspend_always {constexpr bool await_ready() const noexcept {return false; // 返回 false,总是挂起}//等待体其他内容...
};

await_suspend

await_ready 返回 false 时,协程就挂起了。这时候协程的局部变量和挂起点都会被存入协程的状态当中,await_suspend 被调用到。

返回值 await_suspend(std::coroutine_handle<> coroutine_handle);

参数 coroutine_handle 用来表示当前协程,可以在稍后合适的时机通过调用 resume 来恢复执行当前协程:

coroutine_handle.resume();

await_suspend 函数的返回值类型没有明确给出,因为它有以下几种选项:

  • 返回 void 类型或者返回 true,表示当前协程挂起之后将执行权还给当初调用或者恢复当前协程的函数。
  • 返回 false,则恢复执行当前协程。注意此时不同于 await_ready 返回 true 的情形,此时协程已经挂起,await_suspend 返回 false 相当于挂起又立即恢复。
  • 返回其他协程的 coroutine_handle 对象,这时候返回的 coroutine_handle 对应的协程被恢复执行。
  • 抛出异常,此时当前协程恢复执行,并在当前协程当中抛出异常。

await_resume

同样地,await_resume 的返回值类型也是不限定的,返回值将作为 co_await 表达式的返回值。

等待体示例

struct Awaiter {int value;bool await_ready() {// 协程挂起return false;}void await_suspend(std::coroutine_handle<> coroutine_handle) {// 切换线程std::async([=](){using namespace std::chrono_literals;// sleep 1sstd::this_thread::sleep_for(1s); // 恢复协程coroutine_handle.resume();});}int await_resume() {// value 将作为 co_await 表达式的值return value;}
};

await_ready:该方法用来检查协程是否需要挂起。如果返回 false,表示协程应该挂起,进入等待状态。

await_suspend:这个方法在 await_ready 返回 false 时被调用,用于挂起协程。

本样例的具体操作如下:

  • 使用 std::async 创建一个异步任务,该任务会在一个独立的线程上执行。
  • 在这个线程中,首先使线程暂停一秒钟(std::this_thread::sleep_for(1s))。
  • 暂停后,调用 coroutine_handle.resume() 以恢复被挂起的协程。

通过这种方式,await_suspend 方法实现了将协程暂停一段时间的效果,并且不阻塞协程所在的主线程。

await_resume:这个方法在协程恢复执行后被调用,用于传递协程的结果。在这个例子中,它返回结构体中的 value 成员变量,该变量将作为 co_await 表达式的结果。

协程的返回值类型

区别一个函数是不是协程,是通过它的返回值类型来判断的。如果它的返回值类型满足协程的规则,那这个函数就会被编译成协程。

规则就是返回值类型能够实例化下面的模板类型 _Coroutine_traits。

其再#include 文件中有定义如下

template <class _Ret, class = void>
struct _Coroutine_traits {};template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {using promise_type = typename _Ret::promise_type;
};template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

简单来说,就是返回值类型 _Ret 能够找到一个类型 _Ret::promise_type 与之相匹配。这个 promise_type 既可以是直接定义在 _Ret 当中的类型,也可以通过 using 指向已经存在的其他外部类型。

此时,可以给出 Result 的部分实现:

struct Result {struct promise_type {//内部实现 见下文};
};

协程返回值对象的构建

这时已经了解 C++ 当中如何界定一个协程。不过会产生一个新的问题,返回值是从哪儿来的?协程体当中并没有给出 Result 对象创建的代码。

实际上,Result 对象的创建是由 promise_type 负责的,需要定义一个 get_return_object 函数来处理对 Result 对象的创建:

struct Result {struct promise_type {Result get_return_object() {// 创建 Result 对象return {};}//其余函数};
};

不同于一般的函数,协程的返回值并不是在返回之前才创建,而是在协程的状态创建出来之后马上就创建的。也就是说,协程的状态被创建出来之后,会立即构造 promise_type 对象,进而调用 get_return_object 来创建返回值对象。

promise_type 类型的构造函数参数列表如果与协程的参数列表一致,那么构造 promise_type 时就会调用这个构造函数。否则,就通过默认无参构造函数来构造 promise_type。

协程体的执行

initial_suspend

协程体执行的第一步是调用 co_await promise.initial_suspend(),initial_suspend 的返回值就是一个等待对象(awaiter),如果返回值满足挂起的条件,则协程体在最一开始就立即挂起。这个点实际上非常重要,可以通过控制 initial_suspend 返回的等待体来实现协程的执行调度。

有关调度的内容见专栏后续。

协程体的返回情况(值,void,异常)

这个协程的返回并不是返回函数声明前面的类型,而是使用co_return。这个声明的Result更像是沟通协程和调用者的一个桥梁。

接下来执行协程体。协程体当中会存在 co_await、co_yield、co_return 三种协程特有的调用,其中

  • co_await 前面已经介绍过,用来将协程挂起。
  • co_yield 则是 co_await 的另一种实现,用于传值给协程的调用者或恢复者或被恢复者
  • co_return 则用来返回一个值或者从协程体返回。

对于返回一个值的情况,需要在 promise_type 当中定义一个函数

void return_value();//必须返回void

那么再调用co_return 时

co_return 1000;

1000 会作为参数传入,即 return_value 函数的参数 value 的值为 1000。这个值可以存到 promise_type 对象当中,外部的调用者可以获取到。

除了返回值的情况以外,C++ 协程当然也支持返回 void。只不过 promise_type 要定义的函数就不再是 return_value 了,而是 return_void 了:

struct Result {struct promise_type {void return_void() {...}...};
};

此时,协程内部就可以通过 co_return 来退出协程体了:

协程体除了正常返回以外,也可以抛出异常。异常实际上也是一种结果的类型,因此处理方式也与返回结果相似。我们只需要在 promise_type 当中定义一个函数,在异常抛出时这个函数就会被调用到:

struct Result {struct promise_type {void unhandled_exception() {exception_ = std::current_exception(); // 获取当前异常}...};
};

final_suspend

当协程执行完成或者抛出异常之后会先清理局部变量,接着调用 final_suspend 来方便开发者自行处理其他资源的销毁逻辑。final_suspend 也可以返回一个等待体使得当前协程挂起,但之后当前协程应当通过 coroutine_handle 的 destroy 函数来直接销毁,而不是 resume。

一般来说final_suspend应该挂起协程,希望其销毁和result(接受了协程返回值)生命周期一致,避免提前销毁出现意外。

int main() {auto result=Coroutine(); return 0;
} 

测试样例

#define __cpp_lib_coroutine#include <iostream>
#include <coroutine>
#include <future>
#include <chrono>using namespace std::chrono_literals;void Fun() {std::cout << 1 << std::endl;std::cout << 2 << std::endl;std::cout << 3 << std::endl;std::cout << 4 << std::endl;
}struct Result {struct promise_type {int value;std::suspend_never initial_suspend() {return {};}std::suspend_never final_suspend() noexcept {return {};}Result get_return_object() {return {};}void return_value(int _value) {value = _value;std::cout << "coroutine return " << value << '\n';}void unhandled_exception() {}};
};struct Awaiter {int value;bool await_ready() {return false;}void await_suspend(std::coroutine_handle<> coroutine_handle) {std::async([=]() {std::this_thread::sleep_for(1s);coroutine_handle.resume();});}int await_resume() {return value;}
};Result Coroutine() {std::cout << "Coroutine begin " << std::endl;Fun();std::cout << co_await Awaiter{.value = 1000} << std::endl;co_return 5;
};int main() {auto result = Coroutine();std::this_thread::sleep_for(1s);//等待一下看协程返回结果return 0;
}

输出结果

Coroutine begin
1
2
3
4
1000
coroutine return 5

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

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

相关文章

用于扩展Qt自身的插件(下)

扩展Qt自身的插件 引言必须满足项创建插件示例代码生成插件配置加载插件的环境创建使用插件的项目配置库和头文件依赖的步骤:应用程序代码运行结果总结引言 本文继上篇的扩展Qt自身的插件,接着记录Qt自身的插件,只不过本文提及的用于扩展Qt自身的插件是可以在QtCreator的设…

走进MySQL:从认识到入门(针对初学者)

一&#xff0c;引言 MySQL是一款久负盛名且广泛应用的关系型数据库管理系统&#xff0c;自1995年Michael Widenius和David Axmark在瑞典和芬兰发起研发以来&#xff0c;其发展历程可谓辉煌且深远。作为开源软件的代表&#xff0c;MySQL以其卓越的成本效益、高性能及高可靠性赢得…

关于 Amazon DynamoDB 的学习和使用

文章主要针对于博主自己的技术栈&#xff0c;从Unity的角度出发&#xff0c;对于 DynamoDB 的使用。 绿色通道&#xff1a; WS SDK for .NET Version 3 API Reference - AmazonDynamoDBClient Amazon DynamoDB Amazon DynamoDB is a fast, highly scalable, highly available,…

Harmony鸿蒙南向驱动开发-RTC接口使用

功能简介 RTC&#xff08;real-time clock&#xff09;为操作系统中的实时时钟设备&#xff0c;为操作系统提供精准的实时时间和定时报警功能。当设备下电后&#xff0c;通过外置电池供电&#xff0c;RTC继续记录操作系统时间&#xff1b;设备上电后&#xff0c;RTC提供实时时…

【Java SE】11.认识异常

目录 1.异常的概念与体系结构 1.1异常的概念 1.2异常的体系结构 1.3异常的分类 2.异常的处理 2.1防御式编程 2.2异常的抛出 2.3异常的捕获 2.3.1异常声明throws 2.3.2try-catch捕获并处理 2.3.3finally 2.4异常的处理流程 3.自定义异常类 1.异常的概念与体系结构 …

MAC OS关闭SIP(navicat 无法保存密码)

最近安装navicat&#xff08;16.3.7&#xff09;时,安装后无法保存密码,保存密码会报错如下&#xff1a; 因为用的破解版&#xff0c;一开始是打不开的&#xff0c;用自带的修复软件修复后就可以打开了&#xff0c;但是保存密码就会报错&#xff0c;按照网上的一些操作 1、卸载…

蓝桥杯嵌入式(G431)备赛笔记——RTC

// RTC time// 声明一个变量 rtc_tick 用于记录上次 RTC 处理的时间 u32 rtc_tick 0;// 声明结构体变量 D 用于存储 RTC 的日期信息 RTC_DateTypeDef D;// 声明结构体变量 T 用于存储 RTC 的时间信息 RTC_TimeTypeDef T;// RTC_proc 函数&#xff0c;用于处理 RTC 时间 void R…

Github 2024-04-10 C开源项目日报 Top10

根据Github Trendings的统计,今日(2024-04-10统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量C项目10Shell项目1Python项目1Git - 快速、可扩展、分布式的版本控制系统 创建周期:5740 天开发语言:C, Shell协议类型:OtherStar数量:4955…

.NET 设计模式—享元模式(Flyweight Pattern)

简介 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;它旨在减少系统中相似对象的内存占用或计算开销&#xff0c;通过共享相同的对象来达到节省资源的目的。 享元模式提供了一种高效地共享对象的方式&#xff0c;从而减少了内存占用和提…

vue3-video-play 在安卓上正常播放,在ios上不能播放,问题解决

1.ios上autoplay需要静音&#xff0c;在播放后再打开声音 <vue3videoPlay v-if"!isComponent" v-bind"options" :playsinline"playsinline"></vue3videoPlay>let playsinline computed(() > {if (props.isComponent) {return}o…

【电路笔记】-异或门

异或门 文章目录 异或门1、概述2、数字逻辑异或门3、异或门等效电路异或逻辑函数是一个非常有用的电路,可用于许多不同类型的计算电路。 1、概述 异或门是算术运算中常用的另一种数字逻辑门,因为它可以用来给出两个二进制数的和以及错误检测和纠正电路。 在前面的文章中,我…

Photoshop 2023 中文---创意与设计的新篇章

Photoshop 2023是由Adobe Systems开发和发行的一款强大的图像处理软件&#xff0c;广泛应用于专业摄影师、设计师、艺术家等用户群体。它拥有丰富的功能和工具&#xff0c;可以轻松进行图像编辑、合成、调整和修复等任务。在Photoshop 2023中&#xff0c;智能选择功能得到了升级…

python爬虫-------urllib代理和代理池(第十七天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

Kubernetes(k8s):深入理解k8s中的亲和性(Affinity)及其在集群调度中的应用

Kubernetes&#xff08;k8s&#xff09;&#xff1a;深入理解k8s中的亲和性&#xff08;Affinity&#xff09;及其在集群调度中的应用 1、什么是亲和性&#xff1f;2、节点亲和性&#xff08;Node Affinity&#xff09;2.1 硬性节点亲和性规则&#xff08;required&#xff09;…

paddle实现手写数字模型(一)

参考文档&#xff1a;paddle官网文档环境&#xff1a;Python 3.12.2 &#xff0c;pip 24.0 &#xff0c;paddlepaddle 2.6.0 python -m pip install paddlepaddle2.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple调试代码如下&#xff1a; LeNet.py import paddle import p…

013_NaN_in_Matlab中的非数与调试方法

Matlab中的非数与调试方法 是什么&#xff1f; Matlab编程&#xff08;计算器使用&#xff09;中经常有个错误给你&#xff0c;这句话里可能包含一个关键词NaN。大部分学生都有过被 NaN 支配的痛苦记忆。 NaN 是 Not a Number 的缩写&#xff0c;表示不是一个数字。在 Matla…

【SVN】clean up报错:Cleanup failed to process the following paths 解决方法

报错来源&#xff1a;代码更新有一个文件既不能接受自己的也不能接受别人的&#xff0c;只能取消&#xff0c;再提交提醒clean up&#xff0c;随后报标题错误。 解决方法&#xff1a;参考https://www.cnblogs.com/pinpin/p/11395438.html 注&#xff1a;如果clean up的时候有…

基于ssm项目校园快递平台系统

采用技术 基于SpringBoot框架实现的web的智慧社区系统的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringMVCMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 管理员功能 订单管理 快递管理 公告管理 公告类…

基于YOLOv5s的电动车入梯识别系统(数据集+权重+登录界面+GUI界面+mysql)

本人训练的yolov5s模型&#xff0c;准确率在98.6%左右&#xff0c;可准确完成电梯内检测电动车任务&#xff0c;并搭配了GUI检测界面&#xff0c;支持权重选择、图片检测、视频检测、摄像头检测、识别结果拍照和在线标注数据集等功能。 共有4000张左右图片&#xff0c;全部为电…

【ssh】群晖 ssh clone github 远程仓库

群晖 ssh clone github 远程仓库 终端 登录 群晖创建 rsa 密钥对 cd ~/.ssh ssh-keygen -t rsa -C "你的邮箱"查看并复制id_rsa.pub 公钥文件 把复制的内容提交至GitHub-setting-SSH and GPGkeys-Key中本地新建项目文件&#xff0c; 并clone git 代码项目 mkdir /…