c++协程详解(二)

前言

这是c++协程实现第二篇,这里开始我们将开始真正意义上开始实现协程。对协程基础流程不清楚的,可以看我的第一篇。 后续可能需要一定的模板知识,可以看下我的模板的文章,那些知识就完全够用了。本篇将实现一个协程封装的异步任务队列,即一个耗时任务到其他线程完成后,继续恢复执行流。在这里你可以看到如何通过协程去回调。下面就直接开始吧。

协程实现

上一篇我们已经谈过,协程最大的一个好处就是去回调。在工程中,我们往往因为效率,而选择异步接口,或者不希望堵塞任务线程,而将某些耗时任务丢到线程池中执行,我们往往需要传入一个回调,待耗时任务完成后,调用来执行后续操作,但回调的引入,割裂了代码逻辑,大大加重了对业务的理解难度,以及修改流程的难度,在工程中我就深受其害。直接上代码。

代码


#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_resume_queue; //原来的 eventloop队列
std::vector<std::shared_ptr<async_task_base>> g_work_queue; //执行耗时操作线程队列template <typename T>
struct AsyncAwaiter;using namespace std::chrono_literals;struct suspend_always{bool await_ready() const noexcept { try{std::cout << "suspend_always::await_ready" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}return false; }void await_suspend(std::coroutine_handle<> handle) const noexcept {try{std::cout << "suspend_always::await_suspend" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}}void await_resume() const noexcept {try{std::cout << "suspend_always::await_resume" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}}};struct suspend_never{bool await_ready() const noexcept { try{std::cout << "suspend_never::await_ready" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}return true; }void await_suspend(std::coroutine_handle<> handle) const noexcept {try{std::cout << "suspend_never::await_suspend" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}}void await_resume() const noexcept {try{std::cout << "suspend_never::await_resume" << std::endl;}catch(const std::exception& e){std::cerr << e.what() << '\n';}}};struct Result {struct promise_type {promise_type(){std::cout << "promise_type" << std::endl;}~promise_type(){std::cout << "~promise_type" << std::endl;}suspend_never initial_suspend() {std::cout << "initial_suspend" << std::endl;return {};}suspend_never final_suspend() noexcept {std::cout << "final_suspend" << std::endl;return {};}Result get_return_object() {std::cout << "get_return_object" << std::endl;return {};}void return_void() {std::cout << "return_void" << std::endl;}//    void return_value(int value) {//    }void unhandled_exception() {}};
};template <typename ReturnType>
struct  AsyncThread
{using return_type = ReturnType;AsyncThread(std::function<return_type ()>&& func): func_(func){}std::function<return_type ()> func_;
};template <typename ReturnType>
struct async_task: public async_task_base{async_task(AsyncAwaiter<ReturnType> &awaiter):owner_(awaiter){}void completed() override{std::cout << "async_task ::  completed ############" << std::endl;ReturnType result = owner_.func_();owner_.value_ = result;}void resume() override{std::cout << "async_task ::  resume ############" << std::endl;owner_.h_.resume();}AsyncAwaiter<ReturnType> &owner_ ;
};template <typename ReturnType>
struct AsyncAwaiter
{using return_type = ReturnType;AsyncAwaiter(AsyncThread<ReturnType>& info){// std::cout<< " AsyncAwaiter(AsyncThread<ReturnType>& info)" << std::endl;value_ = return_type{};func_ = info.func_;}// 该awaite直接挂起bool await_ready() const noexcept { return flag; }void await_suspend(std::coroutine_handle<> h)  {h_ = h;std::lock_guard<std::mutex> g(m);g_work_queue.emplace_back(std::shared_ptr<async_task_base>( new async_task<uint64_t>(*this)));}return_type await_resume() const noexcept { // std::cout<< "AsyncAwaiter::await_resume" << std::endl;return value_;}bool flag = false;std::function<return_type ()> func_;std::coroutine_handle<> h_; return_type value_ = return_type();
};template<typename T>
inline AsyncAwaiter<T> operator co_await(AsyncThread<T>&& info)
{return AsyncAwaiter(info);
}template <typename ReturnType>
AsyncThread<ReturnType> do_slow_work(std::function< ReturnType () > &&func){return AsyncThread<ReturnType>(std::forward< std::function< ReturnType () > >(func));
}Result Coroutine() {int a = 1;auto func =[&]() -> uint64_t{// std::cout<< "do a slow work !!!!!!!!!!!!!!!!!!!!!" << std::endl;return a;};    uint64_t result = co_await do_slow_work<uint64_t>(func);std::cout << "@@@@@@@@@ result1 is  : " << result  << std::endl;  a = 2;result = co_await do_slow_work<uint64_t>(func);std::cout << "@@@@@@@@@ result2 is  : " << result  << std::endl; a = 3;result = co_await do_slow_work<uint64_t>(func);std::cout << "@@@@@@@@@ result3 is  : " << result  << std::endl; co_return;
};void do_work() {while (1){// 加锁// std::cout << "void do_work()  "   << std::endl;// std::this_thread::sleep_for(std::chrono::seconds(1)); //!!!!!还有这个加锁要在锁钱前不然,让出cpu后,由于还没有解锁,又会被其他线程再拿到锁,这样就死锁了std::lock_guard<std::mutex> g(m);// std::cout << " g_work_queue size " << g_resume_queue.size()   << std::endl;for(auto task : g_work_queue){task->completed();g_resume_queue.push_back(task);}// g_resume_queue.assign(g_work_queue.begin(), g_work_queue.end());   //!!!!!!!这里有个大坑坑查了好久,如果连续两次先进来这里,会把g_raw_work_queue中的元素给清理掉,导致后面无法恢复g_work_queue.clear();// std::cout << " g_resume_queue size " << g_resume_queue.size()   << std::endl;}   }void run_event_loop(){std::vector<std::shared_ptr<async_task_base>> g_raw_work_queue_tmp;while(1){g_raw_work_queue_tmp.clear();// std::this_thread::sleep_for(std::chrono::seconds(1)); {std::lock_guard<std::mutex> g(m);// for(auto &task : g_resume_queue){//     task->resume();// }g_raw_work_queue_tmp.swap(g_resume_queue);}for(auto &task : g_raw_work_queue_tmp){task->resume();}}
}void test_func(){Coroutine();
}int main(){test_func();std::thread work_thread(do_work);run_event_loop();
}

代码分析

我们先从整体上分析下代码,大致分为以下几部分

AsyncThread,AsyncAwaiter,operator co_await :这三个构成了等待体的基本挂起
Result,suspend_always,suspend_never : 这三个构成一个最基本的协程,这三个沿用上一节的内容
g_resume_queue,g_work_queue,do_work,run_event_loop :构成了一个最基础的event_loop + 异步任务队列

接下来我们对这三块进行分析和讲解

do_slow_work

首先我们看下do_slow_work这个函数,这是一个异步函数,可以支持挂起操作,但注意他不是协程函数,这个区分很重要,这直接导致该函数体内是不可以再调用co_await挂起do_slow_work的
在这里插入图片描述
这是个简单的模板函数,该函数的返回值是模板类AsyncThread,AsyncThread的模板参数有参数列表中的函数对象func的返回类型推导出。这个函数很简单,返回了一个AsyncThread对象。这个对象既不是协程类型,也不是等待体,那为什么能挂起当前协程呢?

operator co_await

co_await 是关键字,也是运算符,当co_await的操作数不是awaiter对象时会报错,所以我们需要重载运算符,将AsyncThread转换为
awaiter对象,给co_await使用,这就解释了上面为什么能挂起协程的原因。但这时,你可能又会有疑问,那**do_slow_work为什么不直接返回AsyncAwaiter作为返回值呢?**是的这的确可以,但是我们awaiter作为底层,我们不希望让业务层知道细节,所以用了AsyncThread作为一个代理屏蔽了细节
在这里插入图片描述

AsyncAwaiter

接下来我们自然而然就想知道AsyncAwaiter是怎么实现的
在这里插入图片描述
有了上一篇的基础,我们很容易就可以看出该awaiter对象,必定会挂起,然后将协程句柄保存在awaiter对象中,并创建async_task添加到任务队列g_work_queue中。
当任务完成,调用resume后,会调用await_resume,将完成的值返回出去,模板参数化返回值,以此支持不同类型的返回值,使co_await一个异步函数使用方式尽可能和普通函数相似。

协程体

挂起异步函数不会涉及到和协程体的交互,它只需要提供一个协程作为挂起的承载就够了。这时你一定会直觉上觉得协程体Reuslt作用不应该这么少。是的这里我们留个悬念。

任务队列

任务队列,这里有两个:g_resume_queue和g_work_queue。交互流程是,awaiter添加任务到g_work_queue,工作线程do_work将耗时操作完成后,移交g_resume_queue,由主线程取出调用resume恢复执行。这里有个知识点:协程在哪调用resume,协程就在那个线程中恢复执行,在对有序性要求高的系统中,为了保证有序性,所以我们让主线程来恢复协程执行。

多线程

涉及到多线程,往往问题会很多。工作后,其实基本只在写单线程的代码,这一块经验的不足在这里就暴露出来了。
在这里插入图片描述
1.do_work锁还未释放,就调用了sleep,让出了cpu,run_event_loop线程加锁。造成了死锁
2.g_resume_queue.assign(g_work_queue.begin(), g_work_queue.end()) 这行代码,由于do_work线程连续两次被调度到,导致g_resume_queue中的任务被释放,从而导致挂起点永不恢复。这个和死锁混在一起,真的很难查。
在这里插入图片描述
3.恢复这里写有个坑,之前我习惯性的是这样resume,但这样也出现了死锁问题。当协程中只co_await一次时,不会出现死锁问题。但如果想下面连续挂起多次,就会出现死锁。原因是锁还未解开,你resume执行,代码执行到了第二do_slow_work,这里又执行到了等待体的await_suspend,这里向g_work_queue加了锁,这就是A资源还未解锁对B资源加了锁,又出现了死锁。所以应该在解锁后再恢复协程执行流。
在这里插入图片描述
至此不知不觉把毕业时面试背的几种死锁的情况遇到了个遍。

运行结果

然后我们编译运行下结果,完美执行
在这里插入图片描述

补充

到这里我们再补充下几个知识点

协程挂起,会把用到的参数拷贝存储起来,所以使用lambda捕获参数的时候要特别小心,如果按照引用捕获,一定要确定该对象是协程内的变量,而不是通过普通函数通过引用传进来的,因为引用本身就是指针,所以引用拷贝的时地址,对象本身可能由于函数执行完被析构。
上文的awaiter对象由于是在协程中定义的,所以知道协程执行完之前是不会释放的,所以resume时无需担心内存被释放的问题。

至此我们完成了去回调的目标,这是我们回想那如果我又定义了一个协程,想挂起之前的写协程行不行?
当然可以,但是目前我们编译不行。这个具体实现,我们在下一篇实现。
在这里插入图片描述

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

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

相关文章

Redis慢日志

SLOWLOG 是用来读取和重置 Redis 慢查询日志的命令&#xff0c;Redis 2.2.12 版本开始支持 1.Redis 慢查询日志概述 客户端从发送命令到获取返回结果经过了以下几个步骤&#xff1a; 1. 客户端发送命令 2. 该命令进入 Redis 队列排队等待执行 3. Redis 开始执行命令 - Red…

浅析JavaWeb内存马基础原理与查杀思路

文章目录 前言Java内存马内存马分类&原理JavaWeb三大组件注入Servlet内存马注入Filter型内存马JAVA Agent内存马 哥斯拉木马0x01 WebShell0x02 MemShell0x03 FilterShell0x04 Arthas排查0x05 scanner查杀 总结 前言 几年前写过《Web安全-一句话木马》&#xff0c;主要介绍…

PurpleKeep:提供Azure管道以创建基础设施并执行Atomic测试

关于PurpleKeep PurpleKeep是一款功能强大的安全测试自动化工具&#xff0c;该工具能够通过提供Azure管道以创建基础设施&#xff0c;并帮助广大研究人员执行Atomic测试。 随着攻击技术种类的迅速增加&#xff0c;以及EDR&#xff08;端点检测和响应&#xff09;和自定义检测规…

二叉树层序遍历 及相关题目

1&#xff0c;力扣102 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]]示例…

Canvas背景绘制-24

本节会详细介绍下&#xff0c;如何绘制面板的背景。 概述 常用的技术称为图块复制(blitting)&#xff0c;即从离屏缓冲区中将内容发生变化的那部分背景图像复制到屏幕上&#xff0c;还有其它两种方法是将所有内容擦除并重新绘制&仅重绘内容发生变化的那部分区域。一般是用…

网络:HTTP协议

目录 序列化与反序列化 守护进程 网络计算器的实现 HTTP协议 http的代码演示 HTTPS 初步理解三次握手&#xff0c;四次挥手 ①tcp是面向连接的通信协议&#xff0c;在通信之前&#xff0c;需要进行3次握手&#xff0c;来进行连接的建立(谁connect谁握手) ②当tcp在断开…

稀碎从零算法笔记Day35-LeetCode:字典序的第K小数字

要考虑完结《稀碎从零》系列了哈哈哈 这道题和【LC.42 接雨水】&#xff0c;我愿称之为【笔试界的颜良&文丑】 题型&#xff1a;字典树、前缀获取、数组、树的先序遍历 链接&#xff1a;440. 字典序的第K小数字 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1…

Linux是怎么发送一个网络包的?

目录 摘要 1 从 send 开始 2 传输层 3 网络层 4 网络接口层 4.1 邻居子系统 4.2 网络设备子系统 4.3 软中断发送剩余的 skb 4.4 硬中断又触发软中断 总结 摘要 一个网络包的发送&#xff0c;始于应用层&#xff0c;经层层协议栈的封装&#xff0c;终于网卡。今天来循…

ubuntu18.04图形界面卡死,鼠标键盘失灵, 通过MAC共享网络给Ubuntu解决!

ubuntu18.04图形界面卡死&#xff0c;鼠标键盘失灵&#xff0c; 通过MAC共享网络给Ubuntu解决&#xff01; 1. 尝试从卡死的图形界面切换到命令行界面2. 进入bios和grub页面3. 更改Grub中的设置&#xff0c;以进入命令行4. 在命令行页面解决图形界面卡死的问题5. Mac共享WI-FI网…

【MySQL】数据库的基本操作

目录 一、数据库的库操作 二、数据库的表操作 一、数据库的库操作 数据库的创建 create database (if not exists) 库名 这里的if not exists 是一个判断用的&#xff0c;如果数据库存在&#xff0c;就不执行语句&#xff0c;如果数据库不存在&#xff0c;则执行该语句。 创建…

vulhub中Apache Solr Velocity 注入远程命令执行漏洞复现 (CVE-2019-17558)

Apache Solr 是一个开源的搜索服务器。 在其 5.0.0 到 8.3.1版本中&#xff0c;用户可以注入自定义模板&#xff0c;通过Velocity模板语言执行任意命令。 访问http://your-ip:8983即可查看到一个无需权限的Apache Solr服务。 1.默认情况下params.resource.loader.enabled配置…

C++实现vector

目录 前言 1.成员变量 2.成员函数 2.1构造函数 2.2析构函数 2.3begin,end 2.4获取size和capacity 2.5函数重载【】 2.6扩容reserve 2.7resize 2.8insert 2.9删除 2.10尾插、尾删 3.0拷贝构造函数 3.1赋值运算符重载 前言 自主实现C中vector大部分的功能可以使我们更好的理解并使…

红黑树介绍与模拟实现(insert+颜色调整精美图示超详解哦)

红黑树 引言红黑树的介绍实现结点类insert搜索插入位置插入调整当parent为gparent的左子结点当parent为gparent的右子结点 参考源码测试红黑树是否合格总结 引言 在上一篇文章中我们认识了高度平衡的平衡二叉树AVL树&#xff1a;戳我看AVL树详解哦 &#xff08;关于旋转调整的…

Java 7、Java 8常用新特性

目录 Java 8 常用新特性1、Lambda 表达式2、方法引用2.1 静态方法引用2.2 特定对象的实例方法引用2.3 特定类型的任意对象的实例方法引用2.4 构造器引用 3、接口中的默认方法4、函数式接口4.1 自定义函数式接口4.2 内置函数式接口 5、Date/Time API6、Optional 容器类型7、Stre…

(四) 序列化器类使用整理

从一、序列化器类中&#xff0c;或 视图集源码 中&#xff0c; 可以得知&#xff1a; 序列化器类可以接收一个instance &#xff0c;和一个data serializer_obj XxxxSerializer(instance,datarequest.data) &#xff08;更新时&#xff0c;instance相当于原…

云原生技术精选:探索腾讯云容器与函数计算的最佳实践

文章目录 写在前面《2023腾讯云容器和函数计算技术实践精选集》深度解读案例集特色&#xff1a;腾讯云的创新实践与技术突破精选案例分析——Stable Diffusion云原生部署的最佳实践精选集实用建议分享总结 写在前面 在数字化转型的浪潮下&#xff0c;云计算技术已成为企业运营…

Kafka入门到实战-第五弹

Kafka入门到实战 Kafka常见操作官网地址Kafka概述Kafka的基础操作更新计划 Kafka常见操作 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流平台&…

基于springboot+vue实现的酒店客房管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

昇腾训练执行与推理部署系列 入门: 1.开启异腾AI之旅

一、1认识CANN 1、昇腾AI基础软硬件平台介绍2、CANN逻辑架构介绍 1、昇腾AI基础软硬件平台介绍 2、CANN逻辑架构介绍

普联一面4.2面试记录

普联一面4.2面试记录 文章目录 普联一面4.2面试记录1.jdk和jre的区别2.java的容器有哪些3.list set map的区别4.get和post的区别5.哪个更安全6.java哪些集合类是线程安全的7.创建线程有哪几种方式8.线程的状态有哪几种9.线程的run和start的区别10.什么是java序列化11.redis的优…