【Linux】POSIX信号量与基于环形队列的生产消费者模型

目录

一、POSIX信号量:

接口:

二、基于环形队列的生产消费者模型

环形队列:

单生产单消费实现代码:

RingQueue.hpp:

main.cc:

多生产多消费实现代码:

RingQueue.hpp:

main.cc:


一、POSIX信号量:

在实现线程的同步,互斥不仅仅只有条件变量和锁,还有POSIX信号量,这里学习的POSIX信号量和之前学习的SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX可以用于线程间同步

引入信号量:

对于共享资源,为了保证其并发性,将其分成了几份资源,就允许几个线程进入共享资源访问,此时就引入了信号量来对其进行保护

信号量的本质是一个计数器

这把计数器用来描述临界资源中资源数目的多少,实际上是对资源的预加载机制(这就像在电影院买票,买票的本质就是对电影院座位的预加载机制,当你买到票了,就一定会有位置给你,并且别人即使有票也是坐别人的座位,也不会抢了你的座位)

虽然信号量的本质是一个计数器,当一个线程申请资源成功就将计数器--,当一个线程申请资源失败就将计数器++,但是不能用一个简简单单的普通变量代替信号量,因为变量的--和++操作不是原子的,所以,我们就要使用一个支持PV操作的原子的计数器------信号量

那么什么是PV操作呢?

P:代表申请资源,计数器--

V:代表释放资源,计数器++

当将共享资源分为N份,此时信号量也就是N,这个时候就能够申请资源,再将信号量--,当信号量为0的时候,线程就不能够申请资源了,只能阻塞等待

如上,这是一个多元信号量sem,我们之前学习的锁被叫做二元信号量

在使用多元信号量访问资源的时候,要先申请信号量,只有申请成功了,才能访问资源否则就需要进入阻塞队列等待

接口:

初始化信号量

参数1:需要初始化信号量的地址

参数2:表示的是线程共享还是进程共享,默认为零,线程共享,非零表示进程共享

参数3:设定的信号量的初始值

返回值:初始化成功返回0,失败返回-1,并设置错误码

其中sem_t实际上是一个联合体

销毁信号量

参数:就是需要销毁信号量的地址

返回值:初始化成功返回0,失败返回-1,并设置错误码

申请信号量:

其中下面用的是sem_wait,其功能就是成功将信号量-1,也就是P操作

参数:就是需要销毁信号量的地址

返回值:初始化成功返回0,失败返回-1,并设置错误码

sem_trywait:尝试申请,如果没有申请到资源,就会放弃申请

sem_timedwait:每隔一段时间进行申请

释放信号量(发布信号量)

参数:就是需要销毁信号量的地址

返回值:初始化成功返回0,失败返回-1,并设置错误码

其表示资源使用完毕,归还资源,成功将信号量+1,也就是V操作

二、基于环形队列的生产消费者模型

环形队列:

在实现生产消费者的模型中,不仅仅只有共享队列,还有环形队列,什么是环形队列呢?

虽然它叫环形队列,但是它不是队列,而是用数组实现的,
其中head作为头指针,当申请资源成功的时候就向后移动一位,
tail作为尾指针,当释放资源成功的时候向后移动一位,

首先,如何让数组成环呢?

在每次head++后都进行一次取模操作,这样保证head的大小不会超过这个环形队列的大小

特殊的是,当为空或者为满的时候,头指针和尾指针都指向同一个位置,那么如何证明此时是空还是满呢?

这里有两种方法:

方法一:添加一个计数器,当计数器的值为0的时候,表示当前为空,当计数器的值为容器大小的时候,表示该环形队列为满

判空条件:count == 0
判满条件:count == size

方法二:牺牲一个空间的大小,通过预留一个空位,避免head和tail重合时无法区分空和满。此时队列最大容量为size-1
判空条件:head== tail
判满条件:(head+ 1) % size == tail

在下面实现的时候采用计数器,毕竟信号量是一个天然的计数器

当数据不为空或者满的时候,此时head指针和tail指针必定不指向同一个位置,

此时就能够进行生产者和消费者的同时访问,

为空的时候,只能生产者访问,生产者只关注还剩多少空间

为满的时候,只能消费者访问,消费者只关注还剩多少数据

所以在使用信号量标识资源的情况下,生产者和消费者关注的资源不一样,所以就需要两个信号量来进行计数:

生产者的信号量:表示当前有多少可用空间

消费者的信号量:表示当前有多少可消费数据

所以以下在实现的时候,定义两个信号量,spacesem = N 和datasem = 0

对于生产者的PV操作:P(spacesem)将空间资源-1,V(datasem)将数据资源+1

对于消费者的PV操作:P(datasem)将数据资源-1,V(spacesem)将空间资源+1

单生产单消费实现代码:

RingQueue.hpp:

首先创建一个实现环形队列的文件:

#pragma once
#include <vector>
#include <iostream>
#include <semaphore.h>template <class T>
class RingQueue
{
private:std::vector<T> _ringqueue; // 用vector模拟环形队列int _maxcap;               // 环形队列的最大容量int _p_step; // 生产者下标int _c_step; // 消费者下标sem_t _pspace_sem; // 生产者关注的空间资源sem_t _cdata_sem;  // 消费者关注的数据资源
};

接着依次实现其中的接口:

构造与析构

    RingQueue(int maxcap = 5): _maxcap(maxcap), _ringqueue(maxcap), _p_step(0), _c_step(0){sem_init(&_pspace_sem,0,maxcap);sem_init(&_cdata_sem,0,0);pthread_mutex_init(&_p_mutex,nullptr);pthread_mutex_init(&_c_mutex,nullptr);}~RingQueue(){sem_destroy(&_pspace_sem);sem_destroy(&_cdata_sem);pthread_mutex_destroy(&_p_mutex);pthread_mutex_destroy(&_c_mutex);}

其中,构造函数的主要作用就是初始化各种变量,析构函数的主要作用就是释放这些变量

push与pop

push的作用是从交易场所中放入数据,pop的作用是从交易场所中拿到数据

    void push(const T& in){//生产数据先要申请信号量来预定资源P(_pspace_sem);_ringqueue[_p_step] = in;//将所对应的数据放入到环形队列中_p_step++;//将生产者对应的下标向后移动一位_p_step %= _maxcap;//保证生产者不会超过环形队列的大小V(_cdata_sem);}void pop(T *out){P(_cdata_sem);pthread_mutex_lock(&_c_mutex);*out = _ringqueue[_c_step];//将该位置的数据交给out作为输出型参数带出去_c_step++;//将消费者对应的下标向后移动一位_c_step %= _maxcap;//保证消费者下标不会超过环形队列的大小pthread_mutex_unlock(&_c_mutex);V(_pspace_sem);}

生产者push后,证明环形队列中一定有数据,所以就需要在V后传入消费者关心的信号量,也就是需要传递_cdata_sem

消费者pop后,证明环形队列中一定有空间,所以就需要在V后传入生产者关心的信号量,也就是需要传递_pspace_sem

PV操作:

    void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}

P操作代表申请资源,也就是semwait这个函数

V操作就是释放了资源,比如生产者就是释放了一个数据

这里要保证数据在为空的时候只能生产者运行,在数据为满的时候只能消费者去运行,

所以wait是为了保持顺序同步,保证即使消费者先调用,但是没有数据,就将消费者申请资源所关注的数据信号量送去等待队列里去等待

在封装V操作中,post就是释放资源,对于生产者就是给了个数据给消费者

对于消费者post就是释放了空间,生产者就能接着生产了

那消费者一开始调用P操作,没有数据就会阻塞,而生产者这边V了数据,消费者这边P就不会阻塞了可以拿到数据了

所以生产和消费这两者的PV操作是反的

生产者V了,消费者的p就停止阻塞了因为生产者给了消费者资源了

反之同理

main.cc:

void *Productor(void *args)
{RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);while (true){int data = rand()%10+1;rq->push(data);std::cout<<"Productor : data = "<< data << std::endl;sleep(1);}return nullptr;
}void *Consumer(void *args)
{RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);while (true){int data = 0;rq->pop(&data);std::cout<<"Consumer : data = "<< data << std::endl;sleep(1);}return nullptr;
}int main()
{srand(time(nullptr)^getpid());RingQueue<Task> *rq = new RingQueue<Task>();pthread_t c, p;pthread_create(&p, nullptr, Productor, rq);pthread_create(&c, nullptr, Consumer, rq);pthread_join(p, nullptr);pthread_join(c, nullptr);delete rq;return 0;
}

或者也可以让消费者疯狂消费数据,生产者疯狂生产

多生产多消费实现代码:

RingQueue.hpp:

在多生产多消费中,需要保证生产者和生产者之间、消费者和消费者之间的互斥关系,生产者和消费者之间的互斥关系已经由信号量承担了

所以在多生产多消费的代码中要加上锁

构造与析构中也要增加初始化锁与释放锁

template <class T>
class RingQueue
{
public:RingQueue(int maxcap = 5): _maxcap(maxcap), _ringqueue(maxcap), _p_step(0), _c_step(0){sem_init(&_pspace_sem,0,maxcap);sem_init(&_cdata_sem,0,0);pthread_mutex_init(&_p_mutex,nullptr);pthread_mutex_init(&_c_mutex,nullptr);}~RingQueue(){sem_destroy(&_pspace_sem);sem_destroy(&_cdata_sem);pthread_mutex_destroy(&_p_mutex);pthread_mutex_destroy(&_c_mutex);}private:std::vector<T> _ringqueue; // 用vector模拟环形队列int _maxcap;               // 环形队列的最大容量int _p_step; // 生产者下标int _c_step; // 消费者下标sem_t _pspace_sem; // 生产者关注的空间资源sem_t _cdata_sem;  // 消费者关注的数据资源pthread_mutex_t _p_mutex;//保证生产者和生产者之间的互斥pthread_mutex_t _c_mutex;//保证消费者和消费者之间的互斥
};

push与pop

    void push(const T& in){//生产数据先要申请信号量来预定资源P(_pspace_sem);//信号量的申请本来就是原子的,所以加锁的时候就需要在这之后pthread_mutex_lock(&_p_mutex);_ringqueue[_p_step] = in;//将所对应的数据放入到环形队列中_p_step++;//将生产者对应的下标向后移动一位_p_step %= _maxcap;//保证生产者下标不会超过环形队列的大小pthread_mutex_unlock(&_p_mutex);V(_cdata_sem);}void pop(T *out){P(_cdata_sem);pthread_mutex_lock(&_c_mutex);*out = _ringqueue[_c_step];//将该位置的数据交给out作为输出型参数带出去_c_step++;//将消费者对应的下标向后移动一位_c_step %= _maxcap;//保证消费者下标不会超过环形队列的大小pthread_mutex_unlock(&_c_mutex);V(_pspace_sem);}

细节:

在加锁的时候要在申请信号量之后,这样能够提高并发度

如果是在申请信号量之前进行加锁,那么申请信号量的线程永远只有一个  不能够提高并发度

理解:

就像在电影院中,是先买票在进行排队的,这样能够加快进场的速度,如果排队后再买票,需要一人一人地进行操作,这相比上一种就会很慢的

申请信号量的操作是原子的,不需要加锁保护也能保证线程安全,所以并发申请信号量,串行访问临界资源能够提高并发度

main.cc:

在进行生产消费者模型中的数据问题,不仅仅是让二者看到同一份资源,更重要的是让消费者拿到资源并对资源进行处理,这里引入上一章的Task文件来进行数据处理

Task.hpp

#include <iostream>
#include <string>std::string opers = "+-*/%";enum
{Divzero = 1,Modzero,Unknow
};class Task
{
public:Task(){}Task(int data1, int data2, char oper): _data1(data1), _data2(data2), _oper(oper),_exitcode(0){}void run(){switch (_oper){case '+':_result = _data1 + _data2;break;case '-':_result = _data1 - _data2;break;case '*':_result = _data1 * _data2;break;case '/':if (_data2 == 0)_exitcode = Divzero;else_result = _data1 / _data2;break;case '%':if (_data2 == 0)_exitcode = Modzero;else_result = _data1 % _data2;break;default:_exitcode = Unknow;break;}}void operator()(){run();}std::string Getresult(){std::string ret = std::to_string(_data1);ret += _oper;ret += std::to_string(_data2);ret += "=";ret += std::to_string(_result);ret += "[exitcode=";ret += std::to_string(_exitcode);ret += "]";return ret;}std::string GetTask(){std::string ret = std::to_string(_data1);ret += _oper;ret += std::to_string(_data2);ret += "=?";return ret;}~Task(){}private:int _data1;int _data2;char _oper;int _exitcode;int _result;
};

接着在生产消费者的线程所执行的对应的方法中,基本和上一章类似

void *Productor(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);RingQueue<Task> *rq = td->rq;std::string name = td->threadname;int len = opers.size();while (true){int data1 = rand() % 10 + 1;usleep(10);int data2 = rand() % 10;char op = opers[rand()%len];Task t(data1,data2,op);rq->push(t);std::cout<<"Productor : Task = "<< t.GetTask() << " who "<< name << std::endl;sleep(1);}return nullptr;
}void *Consumer(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);RingQueue<Task> *rq = td->rq;std::string name = td->threadname;while (true){Task t;rq->pop(&t);//处理数据t();std::cout << "Consumer : Task = " << t.GetTask() << " who: " << name << " result: " << t.Getresult() << std::endl;// sleep(1);}return nullptr;
}

我们也可以创建一个结构体来存储线程名称与任务

struct ThreadData
{RingQueue<Task> *rq;std::string threadname;
};
int main()
{srand(time(nullptr));RingQueue<Task> *rq = new RingQueue<Task>();pthread_t c[5], p[3];for(int i = 0;i<3;i++){ThreadData *td = new ThreadData();td->rq = rq;td->threadname = "Productor-" + std::to_string(i);pthread_create(p+i, nullptr, Productor, td);usleep(10);}sleep(1);for(int i = 0;i<5;i++){ThreadData *td = new ThreadData();td->rq = rq;td->threadname = "Consumer-" + std::to_string(i);pthread_create(c+i, nullptr, Consumer, td);usleep(10);}for(int i = 0;i<3;i++){pthread_join(p[i], nullptr);}for(int i = 0;i<5;i++){pthread_join(c[i], nullptr);}return 0;
}

注意:在环形队列中允许多个生产者线程一起进行生活数据,也允许多个消费者线程一起消费数据,多个线程一起操作并非同时操作,任务开始时间有先后,但都是在进行处理的

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

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

相关文章

RAG优化:python从零实现GraphRag 一场文档与知识的“恋爱”之旅

嘿,亲爱的算法工程师们,准备好迎接一场文档与知识的“恋爱”之旅了吗?今天我们要介绍的 Graph RAG,就像是一位“红娘”,帮助文档和知识在图的世界里找到彼此,擦出智慧的火花! 文章目录 为什么需要 Graph RAG?Graph RAG 的“恋爱秘籍”准备好了吗?让我们开始吧!环境设…

深入 SVG:矢量图形、滤镜与动态交互开发指南

1.SVG 详细介绍 SVG&#xff08;Scalable Vector Graphics&#xff09; 是一种基于 XML 的矢量图形格式&#xff0c;用于描述二维图形。 1. 命名空间 (Namespace) ★ 了解 命名空间 URI&#xff1a;http://www.w3.org/2000/svg 用途&#xff1a;在 XML 或 XHTML 中区分不同标…

HTTPS 加密过程详解

HTTPS 的核心组成是 HTTP 协议与 SSL/TLS 加密层的结合&#xff0c;通过加密传输、身份验证和完整性校验机制&#xff0c;确保数据安全。其加密过程通过以下方式保障数据的机密性、完整性和身份验证&#xff1a; 一、HTTPS 的核心组成 1. HTTP 协议 作为基础通信协议&#xf…

嵌入式硬件工程师从小白到入门-速通版(一)

嵌入式硬件工程师从小白到入门&#xff1a;知识点速通与实战指南 一、基础硬件知识体系 电子电路基础 基本概念&#xff1a;电流、电压、电阻、电容、电感等&#xff1b;电路分析&#xff1a;欧姆定律、基尔霍夫定律、戴维南定理&#xff1b;元器件特性&#xff1a;二极管、三极…

SpringBoot通过Map实现天然的策略模式

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; SpringBoot通过Map实现天然的策略模式 ⏱️ 创作时间&#xff1a; 202…

WordPress WooCommerce 本地文件包含漏洞(CVE-2025-1661)

免责声明 仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 一:产品介绍 HUSKY – WooCommer…

matlab近似计算联合密度分布

在 Matlab 中&#xff0c;当A和B是两个序列数据时&#xff0c;可以通过以下步骤来近似求出A大于B的概率分布&#xff1a;数据准备&#xff1a;确保序列A和B具有相同的长度。如果长度不同&#xff0c;需要进行相应的处理&#xff08;例如截取或插值&#xff09;。计算A大于B的逻…

可视化动态表单动态表单界的天花板--Formily(阿里开源)

文章目录 1、Formily表单介绍2、安装依赖2.1、安装内核库2.2、 安装 UI 桥接库2.3、Formily 支持多种 UI 组件生态&#xff1a; 3、表单设计器3.1、核心理念3.2、安装3.3、示例源码 4、场景案例-登录注册4.1、Markup Schema 案例4.2、JSON Schema 案例4.3、纯 JSX 案例 1、Form…

NAT 实验:多私网环境下 NAPT、Easy IP 配置及 FTP 服务公网映射

NAT基本概念 定义&#xff1a;网络地址转换&#xff08;Network Address Translation&#xff0c;NAT&#xff09;是一种将私有&#xff08;保留&#xff09;地址转化为合法公网 IP 地址的转换技术&#xff0c;它被广泛应用于各种类型 Internet 接入方式和各种类型的网络中。作…

C语言-桥接模式详解与实践

文章目录 C语言桥接模式详解与实践1. 什么是桥接模式&#xff1f;2. 为什么需要桥接模式&#xff1f;3. 实际应用场景4. 代码实现4.1 UML 关系图4.2 头文件 (display_bridge.h)4.3 实现文件 (display_bridge.c)4.4 使用示例 (main.c) 5. 代码分析5.1 关键设计点5.2 实现特点 6.…

el-table 合并单元格

vue2使用el-table合并单元格&#xff0c;包括合并行、合并列 <el-table:header-cell-style"handerMethod":span-method"arraySpanMethod"cell-click"handleCellClick":data"tableData"style"width: 100%"><el-tabl…

网络安全之vlan实验

在对vlan进行一定的学习之后我们来练习一个小实验来加深理解记忆 首先是对实验进行一个搭建 第一部分&#xff1a;给交换机配置vlan 首先是sw1 [Huawei]vlan batch 2 to 5 [Huawei]int g0/0/1 [Huawei-GigabitEthernet0/0/1]port hybrid tagged vlan 2 [Huawei-GigabitEthe…

STM32 - 在机器人、自动化领域,LL库相比HAL优势明显

在机器人控制器、电机控制器等领域的开发&#xff0c;需要高实时性、精细化控制或者对代码执行效率、占用空间有较高要求。所以&#xff0c;大家常用的HAL库明显不符合要求。再加上&#xff0c;我们学习一门技术&#xff0c;一定要学会掌握底层的原理。MCU开发的底层就是寄存器…

mysql中show命令的使用

在 MySQL 中&#xff0c;SHOW 命令是一个非常实用的工具&#xff0c;用于查询数据库元数据&#xff08;如数据库、表、列、索引等信息&#xff09;。以下是常见的 SHOW 命令及其用法&#xff1a; 1. 显示所有数据库 SHOW DATABASES;列出服务器上的所有数据库。 2. 显示当前数据…

RAG优化:python从零实现query转换增强技术

本篇仍然是不依赖于LangChain等专用库,利用python基本库实现了三种查询转换技术 查询重写:使查询更加具体和详细,以提高搜索精度。回退提示:生成更广泛的查询以检索有用的上下文信息。子查询分解:将复杂查询分解为更简单的组件,以实现全面检索。图 1:RAG 中的查询重写(…

登录验证码的接口实习,uuid,code.

UID是唯一标识的字符串,下面是百度百科关于UUID的定义&#xff1a; UUID是由一组32位数的16进制数字所构成&#xff0c;是故UUID理论上的总数为16322128&#xff0c;约等于3.4 x 10^38。也就是说若每纳秒产生1兆个UUID&#xff0c;要花100亿年才会将所有UUID用完。 UUID的标准…

HTML5 初探:新特性与本地存储的魔法

HTML5 初探&#xff1a;新特性与本地存储的魔法 作为一名前端新手&#xff0c;你可能听说过 HTML5 这个名词。它是 HTML 的第五代版本&#xff0c;不仅让网页变得更强大&#xff0c;还带来了许多新功能和工具。今天&#xff0c;我们就来聊聊 HTML5 的新特性&#xff0c;以及它…

双指针---《移动零》

目录 文章前言 题目描述 算法原理讲解 忽略限制条件的解法 原理讲解 思路总结 代码展示 双指针解法 原理讲解 思路总结 代码展示 大总结 &#x1f4ab;只有认知的突破&#x1f4ab;才来带来真正的成长&#x1f4ab;编程技术的学习&#x1f4ab;没有捷径&#x1f4ab;…

jangow-01-1.0.1靶机攻略

1.进行配置&#xff0c;按住shift&#xff0c;在图一界面按e进去得到图二 .ro 替换为 rw signie init/bin/bash ctrlx&#xff0c;ip a查看网卡信息&#xff0c;修改配置文件网卡信息 修改为如图所示内容后按shift?然后输入wq点击回车退出&#xff0c;然后重启靶机 2.在kali中…

安全上网沙箱:多方面解决政企私的上网问题

在数字化的浪潮中&#xff0c;网络已成为我们工作与生活不可或缺的一部分。然而&#xff0c;网络的便捷也伴随着诸多安全隐患&#xff0c;尤其是对于企业、个人以及政企机构而言&#xff0c;安全上外网成为了至关重要的课题。 隔离保护&#xff1a;构建安全堡垒 沙箱技术在内网…