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

news2024/10/5 18:33:21

       前文

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

        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.coloradmin.cn/o/1526852.html

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

相关文章

文件系统I/O FATFS RW 源码分析

文件系统I/O FATFS RW 源码分析 0 参考 FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 整个项目都按照 ANSI C (C89) 编写。与存储器 I/O 解耦良好,便于移植到 8051、PIC、AVR、ARM、Z80、RX 等小型微控制器中。 下面是关于 FAT 文件系统格式…

Auto-DataProcessing:一组让制作数据集变轻松的脚本

前言 最近跟同学参加了个比赛,我负责Object-Detection的技术实现,需要从网上扒大量的数据(主办方每种识别物就给了一张demo🤣),发现数据准备是一个真的是一个非常重要但又耗时耗力的过程。对我来说,给我一类待识别的标…

蓝桥之手撕排序算法——冒泡、选择、插入、快排、归并(Python版)

目录 1. 排序引言 2. 冒泡排序 2.1 算法思想 2.2 代码实现 2.3 时空复杂度分析 3. 选择排序 3.1 算法思想 3.2 代码实现 3.3 时空复杂度分析 4. 插入排序 4.1 算法思想 4.3 代码实现 4.4 时空复杂度分析 5. 快速排序 5.1 算法思想 5.2 代码实现 5.3 时空复杂度分…

upload-labs 0.1 靶机详解

下载地址https://github.com/c0ny1/upload-labs/releases Pass-01 他让我们上传一张图片,我们先尝试上传一个php文件 发现他只允许上传图片格式的文件,我们来看看源码 我们可以看到它使用js来限制我们可以上传的内容 但是我们的浏览器是可以关闭js功能的…

【开发】SpringBoot 整合 Redis

目录 前言 1. Redis 的下载及安装 1.1 Redis 的下载 1.2 安装 Redis 1.3 启动 Redis 2. 创建 SpringBoot 项目整合 Redis 2.1 环境要求 2.2 SpringBoot项目构建 2.2.1 方式一 2.2.2 方式二 2.3 在 pom.xml 文件中导入依赖坐标 2.4 在 application.properties 中加…

解释器模式(Interpreter Pattern)

解释器模式 说明 解释器模式(Interpreter Pattern)属于行为型模式,是指给定一门语言,定义它的语法(文法)的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。是一…

CVX安装新版本Mosek求解器

在使用连续凸近似(SCA)求解优化问题时遇到了报错 Problem status : ILL_POSED Solution status : PRIMAL_ILLPOSED_CER并且最后给出的结果为NaN。 在CVX论坛中找到一条回答 具体链接如下: The status is failed 因为我使用的是CVX自带的…

批量查询快递信息,固乔助手助您高效处理

在物流、供应链等行业,每天都需要处理大量的快递信息。无论是物流公司还是供应链企业,都需要实时掌握每一个包裹的状态和位置。然而,当面对成千上万的快递单号时,如何高效地查询这些信息成为了一个亟待解决的问题。 传统的查询方式…

IDEA直接打包Docker镜像

以下为使用IDEA打包Docker镜像并推送到远程仓库(使用Windows打包Docker镜像并推送到远程仓库)教程 1 安装Docker Desktop 下载地址:https://www.docker.com/products/docker-desktop/ 安装成功后,可在cmd查看版本号 2 启动Do…

Machine Learning ---- Feature Scaling

目录 一、What is feature scaling:: 二、Why do we need to perform feature scaling? 三、How to perform feature scaling: 1、Normalization: 2、Mean normalization: 3、Standardization (data needs to follow a normal distribution): 一、What is featur…

膨胀 卷积

1.作用 Dilated convolution、Atrous convolution 增大感受野保持原输入大小 2.膨胀因子 描述的是相邻元素之间的距离 r 2 3.gridding effect 不合理的多个膨胀卷积之前,设计的膨胀因子不合理导致,在增大感受野的同时丢失了细节信息。 丢失&…

【SpringCloud】使用Seata实现分布式事务

目录 一、Seata 框架的需求背景二、Seata 事务模式与架构2.1 Seata 组成2.2 Seata 事务模式 三、Seata 实战演示3.1 部署 Seata Server3.1.1 下载 Seata Server3.1.2 更改 Seata Server 配置3.1.3 创建 Seata Server 所需的数据库、数据库表3.1.4 启动 Seata Server 3.2 Seata …

判断闰年(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>int main() {//初始化变量值&#xff1b;int year 2000;//执行循环判断&#xff1b;while (year < 2010){//执行流程&#xff1b;//判断能否整除4&#xff1…

十二、CyclicBarrier

CyclicBarrier 栅栏 什么时候满了开始发车 CyclicBarrier barrier new CyclicBarrier(20, () -> System.out.println("满人"));满20人开始发车每一个线程到达 barrier.await();都等着当 barrier.await();到达20人就开始往下执行 &#xff0c;发车 public…

struts2 CVE-2021-31805-struts2 s2-061 ONGL远程代码执行复现

复现&#xff1a; 切换到vulhub开启并且查看靶场的端口 访问页面 我们可以输入下面的命令来测试一下&#xff0c;执行6*6&#xff0c;查看源代码发现可以成功运行 接下来我们可以注入下面的payload来进行代码执行&#xff0c;执行id来查看当前用户 POST / HTTP/1.1 Host: x.x.…

探索Spring中的属性注入:@Value注解解析与应用

探索Spring中的属性注入&#xff1a;Value注解解析与应用 探索Spring中的属性注入&#xff1a;Value注解解析与应用摘要引言正文作用代码准备示例注入字符串注入属性注入Bean及其属性 其他属性注入优先级问题对Value属性注入的扩展Spring Boot对Value类型转换的扩展 代码案例演…

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

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

代码随想录算法训练营第二十七天(第二十六天休息)|40.组合总和II

40.组合总和II 题目 给定一个数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 说明&#xff1a; 所有数字&#xff08;包括目标数&#xff09;都是正整数。解集…

Android 开发环境搭建(Android Studio 安装图文详细教程)

Android Studio 下载 https://developer.android.google.cn/studio?hlzh-cn Android Studio 安装 检查电脑是否启用虚拟化 如果没有开启虚拟化&#xff0c;则需要进入电脑的 BIOS 中开启 直接 next选择安装的组件&#xff0c;Android Studio 和 Android 虚拟设备&#xff…

使用蜂鸟地图完成楼层自定义、房间着色、热力图、添加图片覆盖物、添加dom覆盖物、定位到固定区域的中心点

项目里有用到蜂鸟地图的地方&#xff0c;虽然有跟她们对接&#xff0c;但看他们文档也挺费劲的&#xff0c;要自己慢慢研究好久&#xff0c;有些实在研究不出来让他们帮忙看代码发现一些问题&#xff0c;所以把我发现的需要注意的一些点发上来&#xff0c;希望可以帮助到部分有…