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.mzph.cn/news/791905.shtml

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

相关文章

谷歌(Google)技术面试——在线评估问题(三)

谷歌&#xff08;Google&#xff09;面试过程的第一步&#xff0c;你可能会收到一个在线评估链接。 评估有效期为 7 天&#xff0c;包含两个编码问题&#xff0c;需要在一小时内完成。 以下是一些供你练习的在线评估问题。 在本章结尾处&#xff0c;还提供了有关 Google 面试不…

进销存管理系统:食品批发零售迈向数字化未来-亿发

随着消费逐步复苏&#xff0c;食品批发零售行业也迎来了客流的回升&#xff0c;实体店重新焕发了生机。然而&#xff0c;随着数字化时代的来临&#xff0c;传统的食品批发零售企业面临着新的挑战和机遇。些企业正积极实施数字化转型&#xff0c;通过布局线上线下多业态的融合发…

分布式事务之Seata使用

分布式事务解决方案之Seata Seata的概念 Seata是阿里巴巴开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 Seata官网给出的架构示例如下&#…

ruoyi-nbcio-plus基于vue3的flowable流程设计器主界面升级修改

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

mysql故障排查

MySQL是目前企业最常见的数据库之一日常维护管理的过程中&#xff0c;会遇到很多故障汇总了常见的故障&#xff0c;MySQL默认配置无法满足高性能要求 一 MySQL逻辑架构图 客户端和连接服务核心服务功能存储擎层数据存储层 二 MySQL单实例常见故障 故障1 ERROR 2002 (HY000)…

深入理解npm常用命令

npm&#xff08;Node Package Manager&#xff09;是 Node.js 的包管理工具&#xff0c;用于管理 Node.js 应用程序的依赖包。除了安装、更新和卸载依赖包外&#xff0c;npm 还提供了许多其他功能&#xff0c;如初始化项目、运行脚本、查看依赖树等。本文将详细介绍一些常用的 …

RabbitMQ3.x之六_RabbitMQ使用场景

RabbitMQ3.x之六_RabbitMQ使用场景 文章目录 RabbitMQ3.x之六_RabbitMQ使用场景1. 为什么选择 RabbitMQ&#xff1f;1. 可互操作2. 灵活3. 可靠 2. 常见用户案例1. 服务解耦2. 远程过程调用3. 流处理4. 物联网 1. 为什么选择 RabbitMQ&#xff1f; RabbitMQ 是一个可靠且成熟的…

linux------jekins构建cicd

&#x1f388;个人主页&#xff1a;靓仔很忙i &#x1f4bb;B 站主页&#xff1a;&#x1f449;B站&#x1f448; &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;linux &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#…

vue3+threejs新手从零开发卡牌游戏(二十四):添加p2战斗逻辑

用代码模拟p2战斗逻辑&#xff0c;按流程进行步骤拆分&#xff1a; 1.p2抽卡 2.p2召唤怪兽上场 3.p2战斗 其中战斗部分分为几种情况&#xff1a; 情况一&#xff1a;p2场上卡牌由大到小进行排序&#xff0c;按序轮询可以攻击的卡牌&#xff0c;然后攻击p1场上卡牌由大到小…

第19次修改了可删除可持久保存的前端html备忘录:换了一个特别的倒计时时钟

第19次修改了可删除可持久保存的前端html备忘录:换了一个特别的倒计时时钟 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><met…

android framework 学习笔记(1)

学习资料&#xff1a;《Android Framework 开发揭秘》_哔哩哔哩_bilibili 什么是android framework 看图说话&#xff0c;android框架从上至下分为&#xff1a; 应用层(Application)&#xff0c;Java framework(Application Framework),Native framework. 包括Libraries 和 A…

数据透视:将三特征数据集转为矩阵

本文记录利用 wps的excel软件 将包含三个变量的数据集转换成矩阵的表达形式。 1.三特征数据集 三特征数据集/三元数据集&#xff1a;原始数据集的一般表达形式。每一行代表一个样本&#xff0c;每一列代表一个变量&#xff0c;共有3个变量。 2.数据透视表 设置3个变量的行、列和…

CSS3新增的语法(四)

CSS3新增的语法&#xff08;四&#xff09;【布局】 14. 多列布局15.伸缩盒模型1. 伸缩盒模型简介2. 伸缩容器、伸缩项目3. 主轴与侧轴4. 主轴方向5. 主轴换行方式6. flex-flow7. 主轴对齐方式8. 侧轴对齐方式8.1 一行的情况8.2 多行的情况 9.flex 实现水平垂直居中10. 伸缩性1…

【leetcode C++】滑动窗口

1. LCR 008. 长度最小的子数组 题目 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0 。 题目…

“梦该醒了,少年”

顺序表 1、数据结构相关概念2、顺序表2.1、顺序表的概念及结构2.2、顺序表分类2.3、动态顺序表的实现 3、ps:源码 1、数据结构相关概念 数据结构是由“数据”和“结构”两词组合⽽来。 什么是数据&#xff1f; 常⻅的数值1、2、3、4…、教务系统⾥保存的⽤⼾信息&#xff08…

将 Elasticsearch 向量数据库引入到数据上的 Azure OpenAI 服务(预览)

作者&#xff1a;来自 Elastic Aditya Tripathi Microsoft 和 Elastic 很高兴地宣布&#xff0c;全球下载次数最多的向量数据库 Elasticsearch 是公共预览版中 Azure OpenAI Service On Your Data 官方支持的向量存储和检索增强搜索技术。 这项突破性的功能使你能够利用 GPT-4 …

docker-compose运行springinitializr用来创建springboot2

前言 spring initializr官方的地址是: https://start.spring.io/ &#xff0c;这是一个用来创建springboot脚手架的一个工具&#xff0c;但是目前这个工具已经更新到springboot3&#xff0c;而我还没学springboot3&#xff0c;目前还想继续创建springboot2&#xff0c;我就想能…

vue处理后端返回的日志

vue处理后端返回的日志&#xff0c;并保持日志内容最新&#xff08;滚动到最新内容&#xff09; 1、后端返回的日志格式如下所示&#xff0c;该如何处理成正常的文本换行 2、在获取日志的接口中做如下处理&#xff0c;把返回的/n替换成换行标签&#xff0c;并根据任务状态判断…

在Windows的Docker上部署Mysql服务

在我们做一些和数据库相关的测试时&#xff0c;往往需要快速部署一个数据库作为数据源。如果开发环境是Windows&#xff0c;且开发的代码不依赖于系统&#xff0c;即不用在linux上做开发&#xff0c;则可以将全套环境都部署在Windows上。 本地安装数据库会污染操作系统环境&…

算法设计与分析实验报告python实现(串匹配问题、采用分治法求解最大连续子序列和问题、用分治策略求众数问题、最近点对问题)

一、 实验目的 1&#xff0e;加深学生对算法设计方法的基本思想、基本步骤、基本方法的理解与掌握&#xff1b; 2&#xff0e;提高学生利用课堂所学知识解决实际问题的能力&#xff1b; 3&#xff0e;提高学生综合应用所学知识解决实际问题的能力。 二、实验任务 1、串匹配问…