linux线程 | 同步与互斥 | 全解析信号量、环形生产消费者模型

        前言: 本节内容讲述linux下的线程的信号量, 我们在之前进程间通信那里学习过一部分信号量, 但是那个是systemV版本的信号量,是以进程间通信的视角谈的。 但是本篇内容会以线程的视角谈一谈信号量。 

        ps:本篇内容建议学习了生产者消费者模型的友友们进行观看哦。

目录

信号量

信号量接口

初始化信号量

环形队列的生产消费模型

创建文件

RingQueue.h

main.cpp

多生产和多消费的循环队列

RingQueue

main.cpp


 

信号量

         我们之前在快速写一个生产者消费者模型的时候, 我们的交易场所设置的一个队列。 当时我们的queue当作整体使用的, 但是queue只有一份, 所以我们加锁保证了这一份资源的安全性 但是, 我们也要认识到, 共享资源可以被看到多份。 就比如我们今天有一个全局的数组, 有三个线程。 假如数组一共有300个元素,这个数组分为左中右三份。 中间的线程只能访问数组的中间的一份。 左边线程访问左边一份, 右边线程访问右边一份。 因为我们线程各自访问不同的区域, 所以多线程可以同时访问这个数组吗?答案是可以的。

        但是如果是四个线程呢? 我们只有三份资源, 但是要给四个线程使用。 所以呢, 我们就只能放进来三个线程来访问这三个资源。 那么我们的共享资源为了保证并发度, 那么就保持了分成了几份资源, 那么就允许多少个线程进来并发执行。 所以, 为了保护我们的临界资源, 就引入了信号量。 而理解信号量的切入点就是:共享资源也可以被看成多份。

        信号量, 也叫做信号灯, 这把计数器用来描述临界资源中资源数目是多少。 就比如电影院, 里面有很多很多的座位。 假如有100个座位。 我们想要抢占其中的一个座位, 就要提前买一下票!所以, 买票的本质就是对电影院座位资源的预定机制。 那么我们可不可以使用一个变量替换掉这个计数器呢? 答案是不可以, 因为普通变量的++或者--其实不是原子的 多线程并发访问时就会出现问题!!!所以, 我们就要使用一个支持pv操作的原子的计数器——信号量.

        问题是, 信号量的本质就是一把计数器, 而计数器的本质是什么呢? 本质就是临界资源的数量。 所以, 一旦我们用p操作, 我们p操作之后,还用判断资源是就绪的吗? 答案是不需要, 因为只要申请成功了, 就一定有你的。 申请不成功的, 就要去信号量下面去等待了, 所以p操作当中, p操作只要成功, 那么就不需要判断资源就没就绪!所以, 这把计数器的本质是什么? 我们说是用来描述资源数目的, 把资源是否就绪放在了临界区之外申请信号量时, 其实就间接的已经在做判断了!!!

  •         我们之前使用的互斥锁, 其实就可以理解为一个二元信号量, 这个信号量只能为零为一。 一个线程拿到锁后, 只要不释放, 其他的线程无法再次拿到锁!!!

信号量接口

初始化信号量

        第一个参数是信号量对象的地址。 第二个参数代表表示的是线程共享还是进程共享。默认为零,是线程共享。 非零表示进程共享。 第三个参数就是设定的信号量的初始值。

        destroy表示的是清空信号量。         

        sem_wait功能是等待信号量, 将信号量的值减一。 也就是P操作

        sem_post的功能是发布信号量,表示资源使用完毕,该归还资源了, 将信号量加一。也就是V操作。

环形队列的生产消费模型

        我们学习环形队列的生产消费模型的目的是为了理解我们的信号量,以及熟悉一下我们信号量的使用。 其实这里的环形队列就类似于我们的循环队列。 上面的head就是放一个数据就向前走一格子,放一个数据就向前走一个格子。 但是和循环队列不同的是当我们放满的时候两个指针还是指向同一个位置, 因为我们有信号量计数器, 所以不利用指针的指向位置判断空和满!

        我们这里以单生产和单消费为例(因为多生产多消费要复杂, 先实现单生产单消费的代码再来考虑多生产和多消费):

 

        生产和消费只要没有访问同一个位置, 我们就能让他们同时是生产和消费。 也就是说, 我们生产和消费必须遵守三个原则:

  •         1、指向同一个位置的时候, 不能同时访问。
  •         2、消费者不能超过生产者。
  •         3、生产者不能超过消费者一个圈。

那么, 当生产者和消费者在这个追逐的过程中, 什么时候才会指向同一个位置呢? 其实就是生产者生产满了的时候。——》

  •         不空和不满的时候, 指向的一定是不同的位置, 两者可以同时访问!!
  •         为空的时候:只能生产者访问!!
  •         为满的时候:只能消费者访问!!
  •         生产者关注什么资源呢?——还有多少剩余空间!
  •         消费者关注什么资源呢?——还有多少剩余数据!

        在定义信号量的时候,我们可以定义两把信号量, 一个叫做SpaceSem,一个叫做DataSem。 最开始的时候, SpaceSem = MAX DataSem = 0;然后对于生产者来说, 要P(SpaceSem), 就代表生产者生产数据让SpaceSem减一,同时V(DataSem)。 对于消费者来说就P(DataSem), 就代表消费者消耗掉一个数据。

        但是, 当我们的队列为空的时候, 一定是生产者先执行, 然后消费者再来消费。 当生产者生产到SpaceSem为零的时候, 生产者就不能再消费了。下面开始实现代码:

创建文件

        先创建好三个文件, 一个makefile, 一个RingQueue.h用来实现环形队列这个交易场所。然后main.cpp用来实现生产者消费者线程以及生产动作和消费动作

RingQueue.h

        首先我们思考一下我们定义的这个生产消费模型里面要有什么——首先一定要有一个环形队列, 这里我们用vector来模拟。 然后我们要规定环形队列的大小, 所以要有一个变量来规定这个大小, 我们这里定义一个cap_变量。 然后要有一个消费者下标, 一个生产者下标来表示生产者和消费者生产或消费的位置。 最后还要定义两把计数器(信号量)让生产者消费者来预定临界资源。所以代码如下:

#include<iostream>
using namespace std;
#include<vector>
#include<pthread.h>
#include<semaphore.h>const static int defaultcap = 5; //用来初始化cap_template<class T>
class RingQueue
{
public:private:vector<T> ringqueue_;int cap_;   //队列的容量大小int c_step_;    //消费者下标int p_step_;    //生产者下标//注意, 信号量不能保证生产者之间的互斥, 也不能保证消费者之间的互斥。  sem_t cdata_sem_;   //消费者关注的数据资源sem_t pspace_sem_;  //生产者关注的空间资源};

        环形生产消费模型里面有什么方法呢? 首先, 一定要有构造和析构。 然后构造就是对vector, 计数器来进行初始化。 析构同理。 所以代码如下:

#include<iostream>
using namespace std;
#include<vector>
#include<pthread.h>
#include<semaphore.h>const static int defaultcap = 5;template<class T>
class RingQueue
{
public:RingQueue(int cap = defaultcap):cap_(cap), ringqueue_(cap), c_step_(0), p_step_(0){sem_init(&cdata_sem_, 0, 0); sem_init(&pspace_sem_, 0, cap);}~RingQueue(){sem_destroy(&cdata_sem_);sem_destroy(&pspace_sem_);}
private:vector<T> ringqueue_;int cap_;   //队列的容量大小int c_step_;    //消费者下标int p_step_;    //生产者下标//注意, 信号量不能保证生产者之间的互斥, 也不能保证消费者之间的互斥。  sem_t cdata_sem_;   //消费者关注的数据资源sem_t pspace_sem_;  //生产者关注的空间资源};

         然后还有两个方法就是pop和push, push用来向交易场所中加入数据, pop用来从交易场所中拿数据:

void Push(const T& in){//生产数据先要申请信号量空资源P(&pspace_sem_);ringqueue_[p_step_] = in;//维持环形特征p_step_++;p_step_ %= cap_;V(&cdata_sem_);}//在为空和为满的时候就表现出了局部性的互斥特征!!为空的时候,生产者先运行;为满的时候, 消费者先运行——》这不就是生产和消费具有一定的顺序性, 这是局部性的同步。//如果不为空并且不为满,那么我们对应的step两个下标的值一定是不一样的。 两个就叫做并发运行!!!和之前将讲解的不太一样, void Pop(T* out)  //利用指针将数据从队列里面拿出来{P(&cdata_sem_);*out = ringqueue_[c_step_];c_step_++;c_step_ %= cap_;V(&pspace_sem_);}

        这里我们需要思考一下, 首先在为空和为满的时候, 是不是这个时候只能消费者消费, 或者生产者进行生产? 而这,不就是表现出了局部性的互斥特征!!然后为空的时候,生产者先运行;为满的时候, 消费者先运行——》这不就是生产和消费具有一定的顺序性, 这是局部性的同步。        

        然后如果不为空并且不为满,那么我们对应的step两个下标的值一定是不一样的。 两个就叫做并发运行!!!        

main.cpp

        主函数比较简单, 分为三个板块。 一个板块是主函数, 用来创建线程, 以及交易场所。 然后第二三个板块用来定义生产者和消费者线程需要执行的代码。 代码如下:
 

#include"RingQueue.h"#include<unistd.h>
#include<ctime>
#include<iostream>
using namespace std;void* Productor(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);while (true){//获取数据int data = rand() % 10 + 1;//生产数据rq->Push(data);cout << "Productor data done, data is: " << data << endl;}return nullptr;
}void* Consumer(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);while (true){sleep(1);//这里我们的消费者是一秒消费一次。 要知道, 我们的线程跑的是很快的, 所以
//运行后其实生产者一瞬间就能把交易场所打满, 然后就是一秒消费一次, 生产一次!!//消费数据int data = 0;rq->Pop(&data); //处理数据cout << "Comsumer data done, data is: " << data << endl; }return nullptr;
}int main()//常见循环队列RingQueue<int>* rq = new RingQueue<int>();//创建线程pthread_t c, p;//运行线程pthread_create(&c, nullptr, Productor, rq);pthread_create(&p, nullptr, Consumer, rq);//等待线程pthread_join(c, nullptr);pthread_join(p, nullptr);//销毁循环队列delete rq;return 0;
}

运行结果:

多生产和多消费的循环队列

        多生产和多消费要维护它们的互斥关系, 消费者和消费者之间也要维护它们的互斥关系。 如何变成支持多生产和多消费的呢? 答案是加锁!现在我们已经有了生产者和消费者之间的互斥关系。 那么我们只需要再利用锁将生产者之间, 以及消费者之间建立起互斥关系, 就能满足三种关系相互互斥, 就能满足资源的安全。 

RingQueue

#include<iostream>
using namespace std;
#include<vector>
#include<pthread.h>
#include<semaphore.h>const static int defaultcap = 5;template<class T>
class RingQueue
{
private:void P(sem_t* sem){sem_wait(sem);}void V(sem_t* sem){sem_post(sem);}//第一个改动是封装了锁的加锁和解锁void Lock(pthread_mutex_t& mutex){pthread_mutex_lock(&mutex);}void Unlock(pthread_mutex_t& mutex){pthread_mutex_unlock(&mutex);}public:RingQueue(int cap = defaultcap):cap_(cap), ringqueue_(cap), c_step_(0), p_step_(0){sem_init(&cdata_sem_, 0, 0); sem_init(&pspace_sem_, 0, cap);}void Push(const T& in){//生产数据先要申请信号量空资源P(&pspace_sem_);Lock(c_mutex_); //加锁在申请信号量之后比较好ringqueue_[p_step_] = in;//维持环形特征p_step_++;p_step_ %= cap_;Unlock(c_mutex_);V(&cdata_sem_);}//在为空和为满的时候就表现出了局部性的互斥特征!!为空的时候,生产者先运行;为满的时候, 消费者先运行——》这不就是生产和消费具有一定的顺序性, 这是局部性的同步。//如果不为空并且不为满,那么我们对应的step两个下标的值一定是不一样的。 两个就叫做并发运行!!!和之前将讲解的不太一样, void Pop(T* out)  //利用指针将数据从队列里面拿出来{P(&cdata_sem_);Lock(p_mutex_);*out = ringqueue_[c_step_];c_step_++;c_step_ %= cap_;Unlock(p_mutex_);V(&pspace_sem_);}~RingQueue(){sem_destroy(&cdata_sem_);sem_destroy(&pspace_sem_);}
private:vector<T> ringqueue_;int cap_;   //队列的容量大小int c_step_;    //消费者下标int p_step_;    //生产者下标//注意, 信号量不能保证生产者之间的互斥, 也不能保证消费者之间的互斥。  sem_t cdata_sem_;   //消费者关注的数据资源sem_t pspace_sem_;  //生产者关注的空间资源//定义两把锁pthread_mutex_t c_mutex_;pthread_mutex_t p_mutex_;};

上面有三个地方改动, 一个是定义了两把锁变量。 第二个就是封装了加锁和解锁的方法。 第三个就是将push和pop里面申请信号量之后的代码加锁变成了临界区。 这里有一个要思考的点就是为什么加锁要在申请信号量之后? 我们可以这样想, 如果我们加锁在信号量之前, 那么我们的所有的生产者之间在执行push的时候就只能串行执行push。 但是如果我们加锁在信号量之后, 我们的生产者之间就能只串行申请信号量或者串行后面的代码。 就可以令申请信号量和执行后面的代码并行起来!!!所以效率就会提高!!!

main.cpp

主函数改动不大, 就是利用了for循环创建线程:

#include"RingQueue.h"#include<unistd.h>
#include<ctime>
#include<iostream>
using namespace std;void* Productor(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);while (true){//获取数据int data = rand() % 10 + 1;//生产数据rq->Push(data);cout << "Productor data done, data is: " << data << endl;}return nullptr;
}void* Consumer(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);while (true){sleep(1);//消费数据int data = 0;rq->Pop(&data); //处理数据cout << "Comsumer data done, data is: " << data << endl; }return nullptr;
}int main()
{//多生产和多生产之间要维护它们的互斥关系, 消费者和消费者之间也要维护它们的互斥关系。 如何变成支持多生产和多消费呢?//答案是加锁, 现在我们已经有了生产者和消费者之间的互斥关系。 那么我们只需要再利用锁将生产者之间,以及消费者之间建立起//互斥关系, 就能满足三种关系相互互斥, 就能满足资源的安全//常见循环队列RingQueue<int>* rq = new RingQueue<int>();//创建线程pthread_t c[3], p[5];//运行线程for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, Productor, rq);sleep(1);}for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Consumer, rq);sleep(1);}for (int i = 0; i < 3; i++){//等待线程pthread_join(c[i], nullptr);sleep(1);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);sleep(1);}//销毁循环队列delete rq;return 0;
}

然后看一下运行结果(注意, 只看运行结果是看不出来的, 所以这里我们使用了一下监视脚本)

我们可以看右边就会看到我们的线程每一秒就会多一个!!这就是我们的多生产和多消费的情况。 

  ——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!   

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

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

相关文章

Qml-Item的Id生效范围

Qml-Item的Id生效范围 前置声明 本实例在Qt6.5版本中做的验证同一个qml文件中&#xff0c;id是唯一的&#xff0c;即不同有两个相同id 的Item;当前qml文件中声明的id在当前文件中有效&#xff08;即如果其它组件中传入的id&#xff0c;与当前qml文件中id 相同&#xff0c;当前…

国庆旅游高峰期,如何利用可视化报表来展现景区、游客及消费数据

国庆黄金周&#xff0c;作为国内旅游市场的年度盛宴&#xff0c;总是吸引着无数游客的目光。今年&#xff0c;随着旅游市场的强劲复苏&#xff0c;各大景区又再次迎来游客流量的高峰。全国国内出游7.65亿人次&#xff0c;同比增长5.9%&#xff0c;国内游客出游总花费7008.17亿元…

Java | Leetcode Java题解之第485题最大连续1的个数

题目&#xff1a; 题解&#xff1a; class Solution {public int findMaxConsecutiveOnes(int[] nums) {int maxCount 0, count 0;int n nums.length;for (int i 0; i < n; i) {if (nums[i] 1) {count;} else {maxCount Math.max(maxCount, count);count 0;}}maxCou…

一起搭WPF架构之livechart的MVVM使用介绍

一起搭WPF架构之livechart使用介绍 前言ModelViewModelView界面设计界面后端 效果总结 前言 简单的架构搭建已经快接近尾声了&#xff0c;考虑设计使用图表的形式将SQLite数据库中的数据展示出来。前期已经介绍了livechart的安装&#xff0c;今天就详细介绍一下livechart的使用…

前三章例题【现代控制理论】

【现代控制理论-状态空间方程能观性分解】https://www.bilibili.com/video/BV1KU4y1N7jV?p17&vd_source3cc3c07b09206097d0d8b0aefdf07958

如何下载3GPP协议?

一、进入3GPP网页 https://www.3gpp.org/ 二、点击“Specifications &Technologies” 三、点击“FTP Server” 网址&#xff1a; https://www.3gpp.org/specifications-technologies 四、找到“latest”&#xff0c;查看最新版 网址&#xff1a; https://www.3gpp.org/ftp…

【jQuery】jQuery 处理 Ajax 以及解决跨域问题的方式

文章目录 HTTP原生创建 AjaxjQuery 处理 Ajax$.ajax()$().load()$.get()$.post() 跨域CORSJSONPiframeweb sockets HTTP 超文本传输协议&#xff08;HTTP&#xff0c;HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发…

计算机网络易混知识点

1.以太网采用曼彻斯特编码&#xff1b;以太网帧最短为64B&#xff0c;其中14个B首部(目的MAC-6B&#xff0c;源MAC-6B&#xff0c;类型-2B)4B尾部 2.OSI协议中&#xff0c;每一层为上一层提供服务&#xff0c;为下一层提供接口 3.帧序号的比特数表示的是发送窗口的大小&#…

LabVIEW提高开发效率技巧----离线调试

离线调试是LabVIEW开发中一项重要的技巧&#xff0c;通过使用Simulate Signal Express VI生成虚拟数据&#xff0c;开发者能够有效减少对实际硬件的依赖&#xff0c;加速开发过程。这种方法不仅可以提高开发效率&#xff0c;还能降低成本&#xff0c;增强系统的灵活性。 ​ 离…

从零开始使用最新版Paddle【PaddleOCR系列】——第二部分:自建数据集 + 模型微调训练

目录 一、自建数据集 1.官方数据集格式参考 2.自建数据集txt文件编写代码 3.数据集检验 二、模型训练 1.模型配置yaml文件 2.命令行指令训练 在上一篇文章中&#xff0c;构建好了paddleOCR 运行必需的环境&#xff0c;并通过在线下载的方式&#xff0c;使用官方训练好的模型进…

OpenCV图像处理——查找线条的转折点

问题描述 图像中有一条线&#xff0c;如何判断这条线的转折点&#xff1f; 比如下面一张图&#xff1a; 目的是找到图中的三个转折点。 要在图像中检测线的转折点&#xff0c;可以通过分析线的几何形状来完成。这通常需要首先提取线的轮廓&#xff0c;然后根据曲率、角度变化…

D42【python 接口自动化学习】- python基础之函数

day42 高阶函数 学习日期&#xff1a;20241019 学习目标&#xff1a;函数&#xfe63;- 55 高阶函数&#xff1a;函数对象与函数调用的用法区别 学习笔记&#xff1a; 函数对象和函数调用 # 函数对象和函数调用 def foo():print(foo display)# 函数对象 a foo print(a) # &…

JavaWeb Servlet--09深入:注册系统05---动态搜索栏

动态搜索栏 分析&#xff1a;在显示用户信息的表单里有一个下拉框选择用户的信息&#xff0c;一个文本框进行输入&#xff0c;一个按钮就行搜索&#xff0c;在下拉框选择了性别或许姓名的某一个包含字就会返回所有满足的用户。在controller层进行接收选择的搜索条件&#xff0…

三菱PLC伺服-停止位置不正确故障排查

停止位置不正确时&#xff0c;请确认以下项目。 1)请确认伺服放大器(驱动单元)的电子齿轮的设定是否正确。 2&#xff09;请确认原点位置是否偏移。 1、设计近点信号(DOG)时&#xff0c;请考虑有足够为0N的时间能充分减速到爬行速度。该指令在DOG的前端开始减速到爬行速度&…

基于Java微信小程序的的儿童阅读系统的详细设计和实现(源码+lw+部署文档+讲解等)

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

【Linux】解读信号的本质&相关函数及指令的介绍

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

实时语音转文字(基于NAudio+Whisper+VOSP+Websocket)

今天花了大半天时间研究一个实时语音转文字的程序&#xff0c;目的还包括能够唤醒服务&#xff0c;并把命令提供给第三方。 由于这方面的材料已经很多&#xff0c;我就只把过程中遇到的和解决方案简单说下。源代码开源在AudioWhisper: 实时语音转文字(基于NAudioWhisperVOSPWe…

面试八股(自用)

什么是java序列化&#xff0c;什么时候需要序列化? 序列化是指将java对象转化成字节流的过程&#xff0c;反序列化是指将字节流转化成java对象的过程。 当java对象需要在网络上传输 或者 持久化到存储文件中&#xff0c;就需要对java对象进行序列化处理。 JVM的主要组成部分…

[产品管理-46]:产品组合管理中的项目平衡与管道平衡的区别

目录 一、项目平衡 1.1 概述 1.2 项目的类型 1、根据创新程度和开发方式分类 2、根据产品开发和市场周期分类 3、根据风险程度分类 4、根据市场特征分类 5、根据产品生命周期分类 1.3 产品类型的其他分类 1、按物理形态分类 2、按功能或用途分类 3、按技术或创新程…

OceanBase中扩容OCP节点step by step

许多用户在开始使用OceanBase时部署OCP&#xff0c;通常选择单节点部署。但随着后续业务规模的不断扩大&#xff0c;会开始担忧单节点OCP在面对故障时可能丧失对集群运维管控的连续性。鉴于此&#xff0c;会将现有的单节点OCP扩展至多节点部署&#xff0c;以此来确保OCP服务的高…