信号量——生产消费者模型

       前文

        在这一篇博客(信号量博客)中我曾经提及过信号量的知识,而当对信号量进行提炼总结时,大致是以下三点:

        1. 信号量本质是一个计数器(代表资源的数量)

        2. 申请信号量本质就是对资源的一种预定机制

        3. 信号量的PV操作是原子的

        我也在这篇博客(生产消费者模型的实现博客)中介绍过生产消费者模型,这种模型使用了多执行流以生产者负责接受并派发任务,消费者负责执行生产者派发的任务,这种方式能够一定程度上提高代码的执行效率,其中的实现方式根据消费者与消费者之间、生产者与生产者之间、还有消费者与生产者之间的互斥同步关系完成的。

        在本文章中我将对信号量进行较深入的介绍,以及使用信号量重新实现一份新的生产消费者模型。

        信号量

        在实现生产消费者模型博客的过程中,我使用的是阻塞队列来作为一种生产消费模型的基础加以实现的。在这其中,阻塞队列一直是被当作一个整体的资源来被访问,即因为生产者和消费者彼此间互斥,所以同一时刻只能有一个执行流访问阻塞队列,但是我们知道对于队列来说头出尾进看着是生产者生产数据并不会影响消费者消费数据的过程,但其实对于C++中的STL容器来说,容器有可能随时扩容,所以队列本身是线程不安全的,也需要互斥。

        那基于这样的想法,我们可不可以自己开辟一段空间,就将这段空间分成多个块,每个块都是一个基本资源,而每个线程都独自访问属于自己的块资源,那么这么来看的话,这种访问方式本身就是线程安全的不需要使用互斥来控制(因为本质上没有共享资源)。而现在资源的控制就可以转化为使用信号量来表示这段空间中有多少个数据块是可用的,然后当线程需要资源的时候,先申请信号量,然后通过一定操作访问到此时属于自己的资源进行处理,然后释放信号量

        我们在多执行流并发访问共享资源时,都是通过加锁的方式保证线程安全,然而当执行流进入临界区访问共享资源时,一般还要确定该共享资源是否已经准备好了。

加锁之后仍需要判断资源是否已经准备好

        然而对于信号量所管理的资源,我们还需要在申请信号量成功之后再次确认资源是否已经准备好了吗?很明显是不用了,因为信号量的申请成功就意味着此时一定是有资源的。这是与单纯的互斥保护临界资源不同的一点。

        这样的话我们的共享资源好像因为信号量的存在变得能多执行流并发访问了一样。

        信号量的接口

        现在我来简单的介绍一下信号量的接口:

        首先是信号量的初始化函数,第一个参数就是信号量类型为sem_t,第二个参数是表示该信号量是线程内共享还是进程内共享(0是线程内共享),第三个参数就是资源的数量。

        这个是信号量的销毁函数。

        这个函数对应信号量的P操作,即申请信号量,当申请信号量失败时,就会被阻塞在对应的等待队列中。

        这个就是信号量的V操作,即释放信号量。

        生产消费模型

        现在我会介绍如何使用信号量来重新实现一个生产消费者模型,然后这里的生产消费者模型使用的数据机构是循环队列,并且刚开始是单生产单消费。

        我们来想象一下循环队列中的生产消费者模型是什么样的:

        我们知道,生产者负责传递数据,消费者负责接收数据,我们现在来假设这么一种极端情况:生产者一直在生产,而消费者不消费,那么当生产者再次碰到消费者时意味着队列中的数据已经满了,生产者已经不能再生产数据了,这也意味着环形队列中,生产者不能超过消费者一圈。

        而对于消费者而言,假如此时生产者已经将队列中填满数据,此时消费者开始消费数据,当消费者再次碰到生产者时,意味着队列中已经没有有效数据了,也意味着消费者不能超过生产者,要始终在消费者之后。

        我们知道,当线程启动时,操作系统对于执行流的调度是不确定的,但是对于上述模型来说,刚开始也就是队列中没有数据的时候,我们的消费者线程是不能运行的,是要被阻塞的。此时要等待生产者生产数据之后,消费者才能开始执行,而队列中充满数据时,生产者线程是要被阻塞的,直到消费者线程消费了数据之后,生产者线程才能开始执行。这段话中我们发现两种现象:那就是当队列中为空或为满的时候只能由生产者或者消费者访问共享资源。这其中,只能单一执行流访问共享资源体现了互斥,执行执行流访问体现了同步,所以这是需要注意的点。

        但是,除了以上两种极端情况外,惊奇的发现生产者线程和消费者线程是可以并发访问共享资源的,因为它们访问的是共享资源的不同位置。

        假如使用代码实现生产者生产和消费者消费的逻辑该怎么实现呢?

        

        有的人可能就有疑问了,为什么两个线程申请和释放的信号量不是一个而是交叉的啊?

        这其实不难理解,关于两个线程的申请各自的信号量很正常,而对于释放对方的信号量,当生产者生产数据之后,直观的能够看到数据资源增加了一份所以应该对数据信号量进行V操作,而当消费者线程消费数据之后,那个数据的空间就闲置了,空间资源增加了一份所以应该是对空间信号量进行V操作。

        而对于“通过一定操作访问到此时属于自己的资源进行处理” 这个步骤,体现在环形队列中就是,每个元素的位置了,假如环形队列是一个数组的话那就是数组的下标了。由于生产者和消费者在大部分情况下是并行访问队列的,所以生产者和消费者应该有着自己的下标管理。

        关于更多的细节,我们需要在代码中体现了。

        CP模型的实现

现在我们就来简单实现一下,CP模型:

        大致框架与阻塞队列的CP模型一致,但是我们这里使用的不再是互斥量和条件变量,而是信号量,所以我们现在就应该思考一下应该有几个信号量呢?在这里由于生产者和消费者对于“资源”的认识是不同的,生产者所认识的“资源”是队列中的空间,而消费者认识中的“资源”是数据,所以这里我们应该使用两个信号量来表示资源,而对于空间信号量来说初始值应该是队列的大小,资源信号量的初始值是0:

        接下来是生产者生产数据和消费者消费数据的具体逻辑:

至此我们的生产消费者模型就完成了,以下是测试代码:

        在CP模型中还是老生常谈,它不止能传输整形,还能传输任意类型的对象,因为我们使用了模板。

        并且我们发现信号量它很好的作为循环队列的一部分特性而存在,如果没有信号量的话,我们还需要考虑循环队列中是空的还是满的,这需要我们使用额外变量或者空出队列中一个元素格子的代价来实现。

        除此之外我们发现这样由信号量实现的CP模型更加的高效,因为它允许生产者和消费者在大部分时刻下并发访问共享资源。

        多生产消费

        现在就该讨论一下,关于多生产多消费了。

        我们在阻塞队列中的由单生产单消费过渡到多生产多消费时,什么都不用做,因为保护的资源只有一个,而那一个资源已经被保护好了。而在这里,对于消费者和生产者而言它们俩之间的共享资源有信号量和队列,信号量是原子的,而队列又被信号量所保护着,所以这个关系可以还能好的维护,而对于消费者和消费者之间、生产者和生产者之间,它们的共享资源除了原子性的信号量和队列外,还有一个指向队列下标的变量_p_index、_c_index,这两个变量如果不加以保护的话,由会产生数据不安全的问题。所以我们还需要两个锁来保护这两个不同的共享资源。我曾经提过锁的数量一般是与共享资源的而数量挂钩的:

头文件SmartMutex.hpp:

        加入了互斥锁之后,就又出现问题了:到底是先加锁再申请信号量呢,还是先申请信号量再加锁呢?

        当先加锁时,以Push函数为例,多个线程进入Push函数之后,首先要竞争锁资源,竞争到锁资源的线程再进行信号量的申请。

        而先申请信号量时,多个线程先是申请信号量,我们要知道信号量可是有可能有多个的,所以存在多个线程申请信号量成功,然后这些线程只需要竞争锁资源就可以了。

        前者的多个线程是先竞争后单个申请信号量,后者的多个线程前半部分申请信号量可能会存在一步到位的情况,后者再全部进行锁的竞争,竞争到之后直接开始资源的访问。

        这么一看明显是先申请信号量再加锁比较好,所以:

        这样我们就可以实现多生产多消费的CP模型了:

这里我防止打印错误,对打印的过程加了个锁。

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

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

相关文章

final关键字

final关键字 基本介绍final使用细节 基本介绍 final 中文意思:最后的,最终的。 final 可以修饰类、属性、方法和局部变量。 在某些情况下会使用到final: 1) 当不希望类被继承时,可以用 final 修饰; // 如…

Python--成员方法、@staticmethod将成员方法静态化、self参数释义

在 Python 中,成员方法是指定义在类中的函数,用于操作类的实例对象。成员方法通过第一个参数通常命名为 self,用来表示调用该方法的实例对象本身。通过成员方法,可以实现类的行为和功能。 成员方法的定义 在类中定义成员…

【Linux】Linux上代码的编译与调试

目录 Linux上常用的编译器gcc\g 如何使用gcc/g 编译过程: 如何使用gcc编译? 进行预处理 进行编译 进行汇编 进行链接 函数库 函数库的分类 gcc选项 Linux调试器-gdb的使用 gdb的常用参数 Linux项目自动化构建工具make/Makefile 原理 利用…

MYSQL日志 redo_log更新流程 bin_log以及bin_log数据恢复

Redo_log写入策略 Redo log的Innodb_flush_log_at_trx_commit:: 这个参数有三个取值 取值为0:每次事务提交时,只是把redo_log留在 redo log buffer中,宕机会丢失数据; 取值为1(默认值):每次事…

1.中医学习-总论

目录 1.为什么要学中医 2.什么是中医 介绍 中医例子1: 中医例子2: 中医最高境界“大道至简” 中医讲究的是本质 中医核心:阴阳、表里、寒热、虚实 ​编辑医不叩门 3.阴阳 1.一天中的阴阳 2.一年中的阴阳 3.阴阳之间的关系 4.阴阳四季的变化 …

解决:visio导出公式为pdf图片乱码问题

今天需要将Visio编辑好的以后的图输出pdf,但是点击保存后公式部分一直乱码,如下图所示 保存为pdf后会变成: 解决方案:保存时点击文件下方的快速打印,存到桌面,不要直接点击保存

代码随想录算法训练营第二十五天|216.组合总和III,17.电话号码的字母组合

216.组合总和III 题目 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 说明: 所有数字都是正整数。 解集不能包含重复的组合。 示例 1: 输入: k 3, n 7 输出: [[1,2,4]] 示例 2: 输入…

24计算机考研调剂 | 【官方】山东师范大学(22自命题)

山东师范大学2024年拟接收调剂 考研调剂信息 调剂专业目录如下: 计算机技术(085404)、软件工程(085405) 补充内容 我校2024年硕士研究生调剂工作将于4月8日教育部“中国研究生招生信息网”(https://yz.ch…

深入了解JVM底层原理

一、JVM内存结构 1、方法区:存储编译后的类、常量等(.class字节码文件) 2、堆内存:存储对象 3、程序计数器:存储当前执行的指令地址(计算机处理器(CPU)正在执行的下一条指令在内存…

openwrt下部署clouddrive2

在启动项上增加启动参数 在exit 0前面增加 mount --make-shared /mnt/data480g注意,后面的/mnt/data480g要替换成你设置的共享映射券。 拉取镜像 docker pull cloudnas/clouddrive2启动镜像 一定要用ssh在后台用docker run命令启动,因为openwrt前台…

函数-Python

师从黑马程序员 函数初体验 str1"asdf" str2"qewrew" str3"rtyuio" def my_len(data):count0for i in data:count1print(f"字符串{data}的长度是{count}")my_len(str1) my_len(str2) my_len(str3) 函数的定义 函数的调用 函数名&a…

基于Java的大学计算机课程管理平台(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 实验课程档案模块2.2 实验资源模块2.3 学生实验模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 实验课程档案表3.2.2 实验资源表3.2.3 学生实验表 四、系统展示五、核心代码5.1 一键生成实验5.2 提交实验5.3 批阅实…

面试笔记——Redis(缓存击穿、缓存雪崩)

缓存击穿 缓存击穿(Cache Breakdown): 当某个缓存键的缓存失效时(如,过期时间),同时有大量的请求到达,并且这些请求都需要获取相同的数据,这些请求会同时绕过缓存系统&a…

【GameFramework框架内置模块】8、文件系统(File System)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 【GameFramework框架】系列教程目录: https://blog.csdn.net/q7…

登录-前端部分

登录表单和注册表单在同一个页面中,通过注册按钮以及返回按钮来控制要显示哪个表单 一、数据绑定和校验 (1)绑定数据,复用注册表单的数据模型: //控制注册与登录表单的显示, 默认false显示登录 true时显…

【MySQL】4. 表的操作

表的操作 1. 创建表 语法: CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎;说明: field 表示列名 datatype 表示列的类型 character set 字符集&#xff0c…

java方法的引用传递和值传递

1、方法的值参数传递 下面代码,它会在控制台输出什么? public class ArrayTest {public static void main(String[] args) {int number 100;System.out.println(number);change(number);System.out.println(number);}public static void change(int n…

vue3使用qrcodejs2-fix生成背景透明的二维码

qrcodejs官方仓库:GitHub - davidshimjs/qrcodejs: Cross-browser QRCode generator for javascript qrcodejs2-fix 是一个用于生成QR码的JavaScript库,使用的时候先安装,然后通过设置前景色和背景色可以控制显示的二维码效果。想生成透明背…

手撕算法-二叉树的镜像

题目描述 操作给定的二叉树,将其变换为源二叉树的镜像。数据范围:二叉树的节点数 0≤_n_≤1000 , 二叉树每个节点的值 0≤_val_≤1000要求: 空间复杂度 O(n) 。本题也有原地操作,即空间复杂度 O(1) 的解法&#xff0c…

士兵排列问题

解法一&#xff1a; deque实现队头入队和队尾入队即可得到编号排列&#xff0c;每个士兵有二个属性&#xff1a;编号、能力值。 #include<iostream> #include<algorithm> #include<deque> #include<vector> using namespace std; #define endl \n st…