【Linux】信号量(基于环形队列的生产消费模型)

news2024/11/18 16:33:27

文章目录

  • POSIX信号量
    • 一、什么是信号量
    • 二、信号量接口
      • 1.初始化信号量
      • 2.销毁信号量
      • 3.申请信号量(等待信号量)
      • 4.释放信号量(发布信号量)
  • 基于环形队列的生产消费模型
    • 一、结构介绍
    • 二、理论讲解
    • 三、代码实现
  • 总结

POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

一、什么是信号量

在这里插入图片描述
在基于阻塞队列实现生产者消费者模型时,我们发现,在进行生产和消费时,必须先加锁最后解锁,所以在进行生产和消费时,一定是互斥进行的,所有任务都是顺序进行对临界资源的访问,一个临界资源被当成整体来看待的。
但是在有的情况下,一个临界资源可以分成多个资源来使用,此时每个线程可以同时访问被分成的某一个资源,然后可以去同时处理,所以就可以实现并发,只有当多个线程来访问同一份资源时,要通过互斥同步来实现。


那我们怎么知道是否有一份资源呢?
这时就要引入信号量的概念了,信号量本质上是一把计数器,记录的是临界资源的划分的一份一份的临界资源的数目,知道这个数目,我们就可以知道是否有一份资源可以访问,只有满足条件才可以访问。

我们通过下边一个例子来更好的理解信号量:

比如在我们要看电影进行购票时,信号量就相当于电影票的预定机制,在进入放映厅之前,大家就已经买到了票,每个人拿到一张票,就意味着在放映厅内一定有一个座位属于他,但是在卖票时一定要有一定的互斥和同步机制,不能多个人购买同一张票,当票数为0时,就要停止卖票。

转换到今天我们所学的信号量来说:
电影院的所有座位就是一份临界资源,买票的每一个人都是一个线程,而每一个座位就是被划分的一小份资源,一个人买到一张票预定座位就代表着申请到信号量,在卖票时不能把同一张票卖给多个人就是要保证多个线程在访问同一份资源时要有互斥和同步机制,其实,通过信号量我们要实现的是当要看电影时是多个人进入电影院同时看电影,而不是一个人看完一个电影之后另外一个人再进去,这大大提高了效率。

二、信号量接口

1.初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

2.销毁信号量

int sem_destroy(sem_t *sem);
参数:
sem代表要销毁的信号量的指针

3.申请信号量(等待信号量)

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

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

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

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

一、结构介绍

我们在之前学习环形链表的时候接触过这种结构,此处的环形队列本质上一个数组,在物理结构中就是一个线性的数组,而通过我们来进行取模等运算,在逻辑结构中看起来是一个环形的。
但是使用环形结构又会出现一些问题:
在之前学习环形链表时,我们发现如果不进行处理,只是单纯使用begin==end来判断,不能区分是为满还是为空,所以我们选择多开一个空间来解决。
在这里插入图片描述
而在我们这里采用计数器的方式来实现判空和判满。

二、理论讲解

我们之前提到过生产者消费者模型有一个321原则,3就是三种关系,2是2种身份,1是一个交易场所,其实环形队列实现和阻塞队列实现本质上差别就是交易场所不同。
前边提到,当我们将一份临界资源划分为多份之后,就可以让多个线程并发的访问临界资源,所以当消费者和生产者没有指向同一份资源时,他们就是并发的,当指向同一份资源时,说明此时环形队列为空或为满,必须挂起等待。

期望:

生产者不能套圈消费者
消费者不能超过生产者
当为空时,必须让生产者先运行。
当为满时,必须让消费者先运行。
除此之外的情况,都可以并发实现。

并且对于生产者来说,关心的是否有空间资源,而对于消费者来说,关心的是是否有数据资源,每当申请一个数据资源之后,相应空间资源就会加一,而每当申请一个空间资源之后,数据资源就会加一。

生产者申请空间资源,释放数据资源

对于生产者来说,生产者每次生产数据前都需要先申请(等待)空间信号量,
如果空间信号量的值不为0,则信号量申请成功,空间信号量减一,此时生产者可以进行生产操作。
如果空间信号量的值为0,则信号量申请失败,此时生产者需要在空间信号量的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒,当生产者生产完数据后,应该释放B信号量,因为此时队列中的数据已经+1,释放(发布)数据信号量,V操作

==消费者申请数据资源,释放空间资源 ==

对于消费者来说,消费者每次消费数据前都需要先申请(等待)数据信号量,
若数据信号量的值不为0,则信号量申请成功,数信号量减一(即等待空间信号量成功,P操作),此时消费者可以进行消费操作。
如果数据信号量的值为0,则信号量申请失败,此时消费者需要在数据信号量的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
当消费者消费完数据后,应该释放空间信号量,因为此时队列中的数据已经减一,空出一个空间资源,释放(发布)空间信号量,V操作。

三、代码实现

mainTest.cc

#include"ringQueue.hpp"
#include <cstdlib>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;

void* consumer(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while(true)
    {
        sleep(1);
        int x = 0;
        rq->pop(&x);
        cout << "消费: " << x << " [" << pthread_self() << "]" << endl;
        // cout<<"消费"<<x<<endl;
    }
}
void* productor(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;
    while(true)
    {
        
        int in = rand()%30+1;
        cout << "生产: " << in << " [" << pthread_self() << "]" << endl;
        // cout<<"生产"<<in<<endl;
        rq->push(in);
    }
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    RingQueue<int>* rq = new RingQueue<int>();
    pthread_t c[3],p[2];
    pthread_create(c,nullptr,consumer,(void*)rq);
    pthread_create(c+1,nullptr,consumer,(void*)rq);
    pthread_create(c+2,nullptr,consumer,(void*)rq);

    pthread_create(p, nullptr, productor, (void*)rq);
    pthread_create(p+1, nullptr, productor, (void*)rq);

    for(int i = 0; i < 3; i++) pthread_join(c[i], nullptr);
    for(int i = 0; i < 2; i++) pthread_join(p[i], nullptr);
    return 0;
}

ringQueue.hpp

#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "sem.hpp"
using namespace std;

const int gDeafultSize = 5;
template <class T>
class RingQueue
{
public:
    RingQueue(int deafultSize = gDeafultSize)
        : _queue(deafultSize), _nums(deafultSize), _spaceSem(deafultSize), _dataSem(0), c_step(0), p_step(0)
    {
        pthread_mutex_init(&cLock, nullptr);
        pthread_mutex_init(&pLock, nullptr);
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&cLock);
        pthread_mutex_destroy(&pLock);
    }

    void push(const T &x)
    {
        _spaceSem.p();
        pthread_mutex_lock(&pLock);
        _queue[p_step++] = x;
        p_step %= _nums;
        pthread_mutex_unlock(&pLock);
        _dataSem.v();
    }
    void pop(T *out)
    {
        _dataSem.p();
        pthread_mutex_lock(&cLock);
        *out = _queue[c_step++];
        c_step %= _nums;
        pthread_mutex_unlock(&cLock);
        _spaceSem.v();
    }

private:
    vector<T> _queue;
    int _nums;
    Sem _spaceSem;
    Sem _dataSem;
    int c_step;
    int p_step;
    pthread_mutex_t cLock;
    pthread_mutex_t pLock;
};

sem.hpp

#ifndef _SEM_HPP_
#define _SEM_HPP_

#include <iostream>
#include <semaphore.h>

class Sem
{
public:
    Sem(int value)
    {
        sem_init(&sem_, 0, value);
    }
    void p()
    {
        sem_wait(&sem_);
    }
    void v()
    {
        sem_post(&sem_);
    }
    ~Sem()
    {
        sem_destroy(&sem_);
    }
private:
    sem_t sem_;
};

#endif

加粗样式


总结

最后有几个特别关键的问题:
1.为什么有了单生产者和单消费者,还要去实现多生产者多消费者呢?不是在访问时都是互斥的吗?

虽然在加锁后只有一个线程可以访问临界区,但是在加锁之前就已经分配信号量了,不要单纯的认为生产者将数据从私有放入公共空间,消费者从公共空间把数据拿到私有空间就完成了生产和消费,其实最耗费时间的过程在任务产生前和拿到任务之后的处理过程,虽然在拿任务时是一个一个拿的,但是处理任务确实并发的。
就有点像区食堂区吃饭,最耗费时间的过程不是买饭,而是去吃饭,多生产多消费的好处就是虽然买饭时是排队的,但是在,买到饭之后,多个人可以一块吃饭,大大提高了效率。

2.信号量本质上是一把计数器,那么计数器的意义是什么呢?

计数器的意义就是,可以不进入临界区,在外部就可以知道临界资源的情况,只要在外部可以申请到信号量,那么就意味着我们一定可以访问临界资源。

我们再来看前边的阻塞队列:
在这里插入图片描述
首先先申请锁,在进入临界区之后,再去判断条件是否满足,如果不满足就会被挂起,本质就是在外部并不知道临界资源的情况。
而信号量是对临界资源的预定机制,所以在外部一定可以知道临界区的情况,所以可以减少在临界区的判断过程。

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

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

相关文章

C语言之操作符详解

本章重点 1. 各种操作符的介绍 2. 表达式求值 给大家提到一些操作符&#xff0c;下面我们来给大家详细介绍 首先看算术操作符&#xff0c;其他几个都没什么可讲的我们来重点看一下/&#xff08;除法&#xff09; 整数除法&#xff08;除号的两端都是整数&#xff09; 浮点…

【漏洞复现】Apache RocketMQ 命令注入漏洞(CVE-2023-33246)

文章目录 前言声明一、漏洞描述二、漏洞危害三、影响版本四、环境搭建五、漏洞复现六、修复建议 前言 RocketMQ 是阿里巴巴在2012年开发的分布式消息中间件&#xff0c;专为万亿级超大规模的消息处理而设计&#xff0c;具有高吞吐量、低延迟、海量堆积、顺序收发等特点。同时它…

Shell脚本查询进程并kill进程(集群版)

记录&#xff1a;454 场景&#xff1a;使用Shell脚本查询进程并kill进程。使用Shell脚本远程执行脚本查询进程并kill进程。 版本&#xff1a;CentOS Linux release 7.9.2009。 1.使用Shell脚本查询进程并kill进程 1.1脚本 脚本名称&#xff1a;zk-kill_pid.sh 脚本内容&a…

从ROS1到ROS2无人机编程实战指南

亲爱的读者们&#xff0c;我今天非常荣幸地向大家推荐一本本人的全新力作——《从ROS1到ROS2无人机编程实战指南》。这本书站在初学者的角度&#xff0c;从入门到进阶&#xff0c;再到实战&#xff0c;循序渐进&#xff0c;是学习ROS1和ROS2的最佳选择。如今已在全国范围内上市…

Java spring boot 全解Camunda 7,从 0 到 1 构建工作流平台——第一节:各个开源框架对比

目录 1. Camunda 介绍2. Camunda 选型说明2.1 osworkflow2.2 jbpm2.3 ActivitiActiviti介绍各个版本的优缺点 2.4 flowable2.5 camundacamunda介绍主流版本介绍 2.6 n8n.io2.7 为什么选 camunda ? camunda7.x 还是 camunda 8.x &#xff1f;为什么选 camunda&#xff1f;camun…

碳排放预测模型 | Python实现基于机器学习回归分析的碳排放预测模型——数据可视化和探索

文章目录 效果一览文章概述研究内容环境准备源码设计学习总结参考资料效果一览 文章概述 碳排放预测模型 | Python实现基于机器回归分析的碳排放预测模型——数据可视化和探索 目标是测试所选特征对分析的重要性,检测异常值的存在并准备数据以供进一步分析。 </

Netty实战(十二)

预置的ChannelHandler和编解码器&#xff08;二&#xff09;HTTPS、WebSocket的添加使用和大型数据写入以及几种常见的序列化 一、基于Netty的HTTPS程序1.2 使用HTTPS2.3 WebSocket 二、空闲连接和超时三、 解码基于分隔符的协议和基于长度的协议3.1 基于分割符的协议3.2 基于长…

策 略 模 式「指 鼠 为 鸭」

前言 大家好&#xff0c;我是 god23bin&#xff0c;今天我们来介绍下设计模式中的一个重要的设计模式——策略模式。 当涉及到某个行为或算法有多个变体时&#xff0c;策略模式是一种常见的设计模式。它允许在运行时选择使用不同的策略&#xff0c;而无需修改现有代码。 现在…

OneFormer:规则通用图像分割的一个Transformer

文章目录 OneFormer: One Transformer to Rule Universal Image Segmentation摘要本文方法实验结果 OneFormer: One Transformer to Rule Universal Image Segmentation 摘要 通用图像分割并不是一个新概念。过去统一图像分割的尝试包括场景解析、全景分割&#xff0c;以及最…

【工具】SecureCR-8.5下载、安装激活和使用教程(包含常用设置)

目录 一、安装包下载 二、安装教程 三、激活操作 四、使用教程 五、常用设置 一、安装包下载 SecureCRT8.5安装包&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1yy677I99ln_3evoHc5dMXg 提取码&#xff1a;9tyj 二、安装教程 1. 解压、双击进行安装 2. 安装进…

【LeetCode】136. 只出现一次的数 python

目录 题目描述 第一次刷题 第二次刷题 异或运算的规则 题目描述 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;…

[LeetCode周赛复盘] 第 106 场双周赛20230611

[LeetCode周赛复盘] 第 106 场双周赛20230611 一、本周周赛总结6461. 判断一个数是否迷人1. 题目描述2. 思路分析3. 代码实现 6425. 找到最长的半重复子字符串1. 题目描述2. 思路分析3. 代码实现 6426. 移动机器人1. 题目描述2. 思路分析3. 代码实现 6463. 找到矩阵中的好子集…

DHCP是什么?它有什么作用?其工作模式?工作原理?

目录 一、DHCP是什么&#xff1f;二、DHCP的作用&#xff1f;1. 在没有DHCP服务的网络中2. 在有DHCP服务的网络中 三、DHCP的工作模式简介四、DHCP的工作原理五、参考资料 一、DHCP是什么&#xff1f; DHCP是动态主机配置协议&#xff08;Dynamic Host Configuration Protocol…

Vue 封装ajax请求[接口]函数

在Vue项目开发当中&#xff0c;当有了后端提供的数据接口之后呢&#xff0c;就需要来为接口定义接口的请求函数&#xff0c;那么在去定义接口函数之前可以先来封装一个ajax请求函数&#xff1b;可能有的初学者在之前的一些篇目当中看到这个vue发起数据请求的不是使用axios的吗&…

3.MySQL表的增删改查(基础)

文章目录 ☕️1. CRUD☕️&#x1f375;2. 新增&#xff08;Create&#xff09;&#x1f375;&#x1f37c;2.1 单行数据 全列插入&#x1f37c;&#x1f37a;2.2 多行数据 指定列插入&#x1f37a;&#x1f378;2.3关于时间的插入格式(homework数据表)&#xff1a;&#x1f…

串口RS232、RS485最本质区别

由下图可看出不管是RS232还是RS485&#xff0c;其本质都是串口通信&#xff0c;只不过是串口通信电平上的变种而已。所以&#xff0c;我们首先从串口通信讲起。 1、串口通信 任何一种通信都要有物理接口和通信协议。串口通信物理接口如下图&#xff1a; 串口通信协议首先要约…

RBAC权限管理,Shiro实践

概念介绍 RBAC是指基于角色的访问控制&#xff08;Role-Based Access Control&#xff09;&#xff0c;它是一种广泛应用于计算机安全领域的访问控制机制。RBAC通过将用户分配到不同的角色&#xff0c;来控制用户对系统中资源的访问权限。 在RBAC中&#xff0c;每个角色都被…

LVS-DR群集部署

目录 一、LVS-DR数据包流向分析 二、 DR 模式的特点 总结 三、LVS-DR中的ARP问题 1.在局域网中具有相同的IP地址&#xff0c;势必会造成各服务器ARP通信的紊乱 2.路由器根据ARP表项&#xff0c;会将新来的请求报文转发给RealServer&#xff0c;导致Director的VIP失效 3.解…

openGauss5 企业版之yum方式安装

文章目录 1. 支持的架构和操作系统版本2. 使用限制3. 安装方式4. 使用说明 本章节主要介绍在openEuler 22.03 LTS操作系统上&#xff0c;通过yum命令一键安装openGauss数据库。 1. 支持的架构和操作系统版本 x86-64 openEuler 22.03 LTSARM64 openEuler 22.03 LTS 仅在openEu…

【博学谷学习记录】超强总结,用心分享 | 架构师 Maven学习总结

文章目录 Maven基本1.什么是Maven2.为什么用Maven?&#xff08;1&#xff09;jar 包的规模&#xff08;2&#xff09; jar 包的来源&#xff08;3&#xff09;jar 包之间的依赖关系 3.Maven目录结构4.maven仓库配置 Pom层次Pom文件简介Super POM 依赖管理1 依赖传递2 传递性依…