c++20协程详解(三)

前言

前面两节我们已经能够实现一个可用的协程框架了。但我们一定还想更深入的了解协程,于是我们就想尝试下能不能co_await一个协程。下面会涉及到部分模板编程的知识,主要包括(模板偏特化,模板参数列表传值,模板函数类型推断)可以提前了解下,方便后续理解。

在开始之前我们不妨思考下面一些问题:

  • 异步函数协程 在co_await时的共性是什么?
    两者的操作数都必须是awaiter,这意味着co_await协程,必须能够将协程转换为一个awaiter。
  • 协程生命周期什么时候结束?协程执行到co_return时,或者执行coroutine_handle.destory()时,以及出现未捕获的异常时会销毁协程,释放协程对象promise
  • 协程从开始到结束,会产生几个awaiter,会co_awiter几次?
    2~3次,initial_suspend和final_suspend会产生两个awaiter,同时编译器帮我们进行了co_await, 还有一次是人为的co_await,即我们co_await一个协程。
  • initial_suspend和final_suspend 这两个功能的作用是什么?
    为什么要initial_suspend和final_suspend ,或者说为什么这里可以自由返回可挂起的等待体,为什么提供这个机制?initial_suspend是协程创建后,编译器帮我们co_await,这将允许我们即使不使用co_await,协程函数运行能参与到不同于普通函数的调度中,这直接决定了协程行为上和普通函数的相似程度;final_suspend功能也类似,我们已经知道该函数是在协程执行结束时操作系统使用co_await调用的,如果final_suspend返回的是不挂起操作awaiter,那么协程在执行完后会自动析构promise对象释放资源,而返回挂起awaiter,提供了将协程对象销毁交给用户的协程调度器的可能性。这里还有一个知识点,是对第二篇final_suspend的补充,使用协程还可以实现序列发生器,序列发生器中的协程永远不会调用co_return,所以永远不会结束,当final_suspend不挂起时,编译器也无法分辨出一个协程的生命周期,而这里选择挂起,我们可以明确告诉编译器该协程会结束,有助于编译器帮我们优化。

协程代码实现

我的目标是让c++像在python中一样使用协程。
我的协程实现思路如下:我希望协程表现出的行为尽可能和普通函数一样,所以我不在initial_suspend时挂起协程给协程调度器调度(我直接在该返回的awaiter::await_ready返回true,给编译器提供优化协程为inline的机会);协程应该是和python一样是单线程,所以不会在用户co_await时,交给其他线程处理(这部分功能由上一节异步函数补齐);我希望编译器尽可能能帮我优化代码而且更符合规范,所以我在协程结束时挂起协程,交给调度器去调度销毁。

代码

#include <coroutine>
#include <future>
#include <chrono>
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>struct async_task_base
{virtual void completed() = 0;virtual void resume() = 0;
};std::mutex m;
std::vector<std::shared_ptr<async_task_base>> g_event_loop_queue; std::vector<std::shared_ptr<async_task_base>> g_resume_queue; //多线程异步任务完成后后,待主线程恢复的线程
std::vector<std::shared_ptr<async_task_base>> g_work_queue; //执行耗时操作线程队列enum class EnumAwaiterType:uint32_t{EnumInitial = 1, //协程initialEnumSchduling = 2,// 用户co_awaitEnumFinal = 3//销毁
};template <typename ReturnType>
struct CoroutineTask;template <typename CoTask, EnumAwaiterType AwaiterType >
struct CommonAwaiter ;template <typename CoTask, EnumAwaiterType AwaiterType>
struct coroutine_task: public async_task_base{coroutine_task(CommonAwaiter<CoTask, AwaiterType> &awaiter):owner_(awaiter){}void completed() override{}void resume() override{if(owner_.h_.done()){owner_.h_.destroy();}else{owner_.h_.resume();}}CommonAwaiter<CoTask,AwaiterType> &owner_ ;
};template <typename CoTask, EnumAwaiterType AwaiterType = EnumAwaiterType::EnumSchduling>
struct CommonAwaiter 
{using return_type =  typename CoTask::return_type;using promise_type = typename CoTask::promise_type;CommonAwaiter(promise_type* promise):promise_(promise){}// 当时initial_suspend返回的awaiter时,挂起,直接resumebool await_ready() const noexcept { return false;}//也可以直接恢复 // std::coroutine_handle<> await_suspend(std::coroutine_handle<> h)  {//     return h;// }void await_suspend(std::coroutine_handle<> h)  {// std::cout <<"await_suspend()" << std::endl;h_ = h;g_event_loop_queue.emplace_back(std::shared_ptr<async_task_base>( new coroutine_task<CoTask, AwaiterType>(*this)) );}return_type await_resume() const noexcept { return promise_->get_value();}~CommonAwaiter(){}bool resume_ready_= false;promise_type* promise_ = nullptr;std::coroutine_handle<> h_ = nullptr;
};template <typename CoTask>
struct CommonAwaiter<CoTask, EnumAwaiterType::EnumInitial>
{CommonAwaiter(){}// 当时initial_suspend返回的awaiter时,挂起,跳过await_suspend,直接resume,跳过bool await_ready() const noexcept { return true;}void await_suspend(std::coroutine_handle<>)  {}void await_resume() const noexcept { }~CommonAwaiter(){}
};// 必须为noexcept,因为这个时候协程已经运行结束,不该有异常产生
template <typename CoTask>
struct CommonAwaiter <CoTask, EnumAwaiterType::EnumFinal>
{CommonAwaiter(){}// 这里不选择true让编译器帮我们自动释放,如果为true编译器不知道什么时候协程结束,无法帮助我们优化bool await_ready() noexcept { return false;}void await_suspend(std::coroutine_handle<> h)  noexcept{h_ = h;g_event_loop_queue.emplace_back(std::shared_ptr<async_task_base>( new coroutine_task<CoTask, EnumAwaiterType::EnumFinal>(*this)));}// 无需返回void await_resume()  noexcept{ }std::coroutine_handle<> h_ = nullptr;
};template<typename CoTask>
struct Promise
{using return_type  = typename CoTask::return_type ;~Promise(){//    std::cout << "~Promise" << std::endl;}CommonAwaiter<CoTask, EnumAwaiterType::EnumInitial> initial_suspend() {return {}; };CommonAwaiter<CoTask, EnumAwaiterType::EnumFinal> final_suspend() noexcept { return {}; }// 提供了一种对协程中未捕获的异常的再处理,比如将异常保存下来,实现协程如以下形式 : coroutine().get().catch()// 这里我们的实现形式决定了,这里直接再次抛出异常就好void unhandled_exception(){// try {std::rethrow_exception(std::current_exception());// } catch (const std::exception& e) {//     // 输出异常信息//     std::cerr << "Unhandled exception caught in CustomAsyncTask: " << e.what() << std::endl;// } catch (...) {//     std::cerr << "Unhandled unknown exception caught in CustomAsyncTask!" << std::endl;// }}CoTask get_return_object(){ return  CoTask(this);}return_type get_value() {return value_;}void return_value(return_type value){value_ = value;}// 该代码写在Promise中的好处是,可以方便阅读代码很容易就能回想出协程最多会返回三个等待体template<typename T>CommonAwaiter<CoroutineTask<T>> await_transform(CoroutineTask<T> &&task){return CommonAwaiter<CoroutineTask<T>>(task.p_);}CoTask await_transform(CoTask &&task){return CommonAwaiter<CoTask>(task.p_);}return_type value_;
};template <typename ReturnType>
struct CoroutineTask{using return_type  = ReturnType;using promise_type = Promise<CoroutineTask>;CoroutineTask(const CoroutineTask &other) = delete;CoroutineTask(const CoroutineTask &&other) = delete;CoroutineTask& operator=(const CoroutineTask&) = delete;CoroutineTask& operator=(const CoroutineTask&&) = delete;CoroutineTask(promise_type* promise) {p_ = promise;}promise_type *p_ = nullptr;};CoroutineTask<u_int64_t> second_coroutine(){co_return 3;
}CoroutineTask<float> third_coroutine(){co_return 3.1;
}CoroutineTask<char> first_coroutine(){uint64_t num =  co_await second_coroutine();std::cout << "second_coroutine result is  : " << num  << std::endl; float num2 =  co_await third_coroutine();std::cout << "third_coroutine result is  : " << num2  << std::endl; co_return 'b';
}void do_work() {while (1){std::lock_guard<std::mutex> g(m);for(auto task : g_work_queue){task->completed();g_resume_queue.push_back(task);}g_work_queue.clear();}   }void run_event_loop(){std::vector<std::shared_ptr<async_task_base>> g_raw_work_queue_tmp;std::vector<std::shared_ptr<async_task_base>> g_event_loop_queue_temp;while(1){g_raw_work_queue_tmp.clear();g_event_loop_queue_temp.clear();{g_event_loop_queue_temp.swap(g_event_loop_queue);std::lock_guard<std::mutex> g(m);g_raw_work_queue_tmp.swap(g_resume_queue);}// 优先恢复耗时任务for(auto &task : g_raw_work_queue_tmp){task->resume();}for(auto task : g_event_loop_queue_temp){task->resume();}}
}void test_func(){first_coroutine();
}int main(){test_func();std::thread work_thread(do_work);run_event_loop();return 0;
}

代码分析

Promise

在这里插入图片描述

unhandled_exception

unhandled_exception 的作用是是用来对协程中未捕获的异常再处理。在一些实现协程使用方式为 **coroutine().get().catch()**的架构中,会把未捕获的异常暂存下来,待恢复的时候再抛出。我选择直接抛出异常,因为出现未捕获的异常时,协程也会提前结束,这时reume的结果是未定义的,所以我觉得在resume之前抛出异常有有必要的。

在这里插入图片描述

await_transform

await_transform的作用和重载运算符co_await是一样的,在co_await一个协程时,会转换CoroutineTask为一个awaiter。使用await_transform的优势是,所有等待体的返回时机,都在promise定义出来,方便代码阅读。
这里我们需要注意的是该await_transform需要定义为模板函数,而不能用Promise的类型参数CoTask,作为传入参数类型。

修改代码如下
在这里插入图片描述
编译一下,我们发现报错了
在这里插入图片描述
这里我们再结合代码理解下
在这里插入图片描述
根据报错信息和调用顺序,我们可以得出以下结论:

当前位于CoroutineTask的写成体中,所以对应的promise类型是promise<CoroutineTask>,
这时实例化的await_transform 实际上是 CoroutineTask await_transform (CoroutineTask&&task),而这时await_transform 操作的是协程second_coroutine,协程类型是CoroutineTask<u_int64_t> 类型不一致,所以会出现上面的报错。

CoroutineTask

在这里插入图片描述
协程类CoroutineTask要保存什么?
这里只保存了promise的指针,原因如下:

协程和用户代码交互是通过awaiter对象,由于返回值是通过return_value保存在协程promise中的,我们需要在awaiter从promise获取返回的值,所以需要在awaiter中保存promise的指针,那promise的指针从哪来呢?awaiter是在await_transform中使用CoroutineTask初始化的,而我们又知道CoroutineTask是由promise 调用 get_return_object创建的。所以我们在创建CoroutineTask时,将promise的指针保存进去, 这样awaiter就能够通过CoroutineTask作为中介得到promise的指针啦!

CommonAwaiter

其实讲到这里,CommonAwaiter就没多少能讲的东西了。
awaiter使用偏特化,根据不同枚举,特化了三个版本。来控制协程的基本行为:即创建时不挂起,能够有机会被编译器优化为inline;用户代码挂起能够返回任意co_return返回的值;结束挂起,参与调度销毁。对了还有一个问题协程句柄为什么保存在awaiter中而不是promise中。在我看来awaiter就代表了每个挂起点,所以将couroutine_handle保存在awaiter中,couroutine_handle很小所以不考虑内存开销

运行结果

最后我们运行下代码,完美运行。
这里就不阐述流程了,下一篇会将二、三两节的代码合并起来,集中阐述下流程和汇总下重要的知识点。
在这里插入图片描述

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

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

相关文章

理论实践-CPU性能监控工具-uptime-mpstat-pidstat-vmstat-top-ps-perf

CPU 性能工具。 首先&#xff0c;平均负载的案例。我们先用 uptime&#xff0c; 查看了系统的平均负载&#xff1b;而在平均负载升高后&#xff0c;又用 mpstat 和 pidstat &#xff0c;分别观察了每个 CPU 和每个进程 CPU 的使用情况&#xff0c;进而找出了导致平均负载升高的…

risc-v向量扩展strlen方法学习

riscv向量文档中给出了strlen的实现&#xff0c; 大概是这么一个思路&#xff0c; 加载向量: 使用向量加载指令&#xff08;如 vload&#xff09;从内存中加载一个向量长度的字符。比较向量与零: 使用向量比较指令&#xff08;如 vmask 或 vcmpeq&#xff09;来检查向量中的每…

【Spring篇】Spring IoC DI

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Spring系列】 本专栏旨在分享学习Spring MVC的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 前言一、IoC二、…

HTMLCSSJS

HTML基本结构 <html><head><title>标题</title></head><body>页面内容</body> </html> html是一棵DOM树, html是根标签, head和body是兄弟标签, body包括内容相关, head包含对内容的编写相关, title 与标题有关.类似html这种…

STM32-05基于HAL库(CubeMX+MDK+Proteus)串行通信案例(中断方式接收命令)

文章目录 一、功能需求分析二、Proteus绘制电路原理图三、STMCubeMX 配置引脚及模式&#xff0c;生成代码四、MDK打开生成项目&#xff0c;编写HAL库的功能代码五、运行仿真程序&#xff0c;调试代码 一、功能需求分析 在中断机制实现按键检测的案例之后&#xff0c;我们介绍串…

Flink运行机制相关概念介绍

Flink运行机制相关概念介绍 1. 流式计算和批处理2. 流式计算的状态与容错3. Flink简介及其在业务系统中的位置4. Flink模型5. Flink的架构6. Flink的重要概念7. Flink的状态、状态分区、状态缩放&#xff08;rescale&#xff09;和Key Group8. Flink数据交换9. 时间语义10. 水位…

sky06笔记下

1.边沿检测 检测输入信号din的上升沿&#xff0c;并输出pulse module edge_check ( clk, rstn, din, pulse ); input wire clk,rstn; input wire din; output reg pulse;wire din_dly;always (posedge clk or negedge rstn)beginif(!rstn)din_dly < 1b0;elsedin_dly < d…

【Qt】:常用控件(四:显示类控件)

常用控件 一.Lable二.LCD Number 一.Lable QLabel 可以⽤来显⽰⽂本和图⽚. 代码⽰例:显⽰不同格式的⽂本 代码⽰例:显⽰图⽚ 此时,如果拖动窗⼝⼤⼩,可以看到图⽚并不会随着窗⼝⼤⼩的改变⽽同步变化 为了解决这个问题,可以在Widget中重写resizeEvent函数。当用户把窗口从A拖…

【Android、 kotlin】kotlin学习笔记

基本语法 fun main(){val a2var b "Hello"println("$ (a - 1} $b Kotlin!")} Variables 只赋值一次用val read-only variables with val 赋值多次用var mutable variables with var Standard output printin() and print() functions String templ…

【JavaScript】函数 ⑦ ( 函数定义方法 | 命名函数 | 函数表达式 )

文章目录 一、函数定义方法1、命名函数2、函数表达式3、函数表达式示例 一、函数定义方法 1、命名函数 定义函数的标准方式 就是 命名函数 , 也就是之前讲过的 声明函数 ; 函数 声明后 , 才能被调用 ; 声明函数的语法如下 : function functionName(parameters) { // 函数体 …

SpringBoot整合ELK8.1.x实现日志中心教程

目录 背景 环境准备 环境安装 1.JDK安装 2.安装Elasticsearch 3.安装zookeeper 4.安装Kafka 5.安装logstash 6.安装file beat 解决方案场景 1.日志采集 1.1 应用日志配置 1.1.1 创建logback-spring.xml文件 1.1.2 创建LoggerFactory 1.1.3 trace日志的记录用法 …

flutter官方案例context_menus【搭建与效果查看】【省时】

案例地址 https://github.com/flutter/samples/tree/main/context_menus 1&#xff1a;运行查看有什么可以快捷使用的&#xff0c;更新了些什么&#xff0c;可不可以直接复制粘贴 主要内容&#xff1a;在web端中模拟手机类型的点击长按操作&#xff0c;不能直接运行在安卓与io…

解决VScode中matplotlib图像中文显示问题

一、更改配置文件 参考这个文件路径找到自己Python环境下的matplotlibrc文件并用记事本打开。 用ctrl F寻找下面的这两行并将前面的#删除&#xff0c;保存并退出。 font.family: sans-serif font.serif: DejaVu Serif, Bitstream Vera Serif, Computer Modern Roman, N…

基于springboot实现校园资料分享平台系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现校园资料分享平台演示 摘要 随着信息互联网购物的飞速发展&#xff0c;国内放开了自媒体的政策&#xff0c;一般企业都开始开发属于自己内容分发平台的网站。本文介绍了校园资料分享平台的开发全过程。通过分析企业对于校园资料分享平台的需求&#xff0c;创…

WPF中通过自定义Panel实现控件拖动

背景 看到趋时软件的公众号文章&#xff08;WPF自定义Panel&#xff1a;让拖拽变得更简单&#xff09;&#xff0c;发现可以不通过Drag的方法来实现ListBox控件的拖动&#xff0c;而是通过对控件的坐标相加减去实现控件的位移等判断&#xff0c;因此根据文章里面的代码,边理解边…

跳跃游戏-java

题目描述: 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 解题思想: …

Android 性能优化之黑科技开道(一)

1. 缘起 在开发电视版智家 App9.0 项目的时候&#xff0c;发现了一个性能问题。电视系统原本剩余的可用资源就少&#xff0c;而随着 9.0 功能的进一步增多&#xff0c;特别是门铃、门锁、多路视频同屏监控后等功能的增加&#xff0c;开始出现了卡顿情况。 经过调研分析发现有…

【蓝桥杯嵌入式】RTC——实时时钟

一、RTC简介 RTC RTC—real time clock&#xff0c;实时时钟&#xff0c;主要包含日历、闹钟和自动唤醒这三部分的功能&#xff0c;其中的日历功能我们使用的最多。日历包含两个32bit的时间寄存器&#xff0c;可直接输出时分秒&#xff0c;星期、月、日、年。 从Cubemx里的配置…

HTTPS传输过程

HTTPS&#xff1a;超文本传输安全协议 相较于HTTP明文传输&#xff0c;HTTPS增加了SSL/TLS进行了加密增加了通信的安全性。 SSL和TLS是两个不同的加密方法&#xff0c;SSL是TLS的前身&#xff0c;现在绝大多数浏览器使用的是TLS&#xff0c;所以着重了解以下TLS的概念即可。 首…

接口测试之测试原则、测试用例、测试流程......

一、接口的介绍 软件测试中&#xff0c;常说的接口有两种&#xff1a;图形用户接口&#xff08;GUI&#xff0c;人与程序的接口&#xff09;、应用程序编程接口&#xff08;API&#xff09;。 接口&#xff08;API&#xff09;是系统与系统之间&#xff0c;模块与模块之间或者…