【Hello Linux】信号量

作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:简单介绍linux中信号量的概念

信号量

  • 信号量的概念
  • 信号量的使用
  • 信号量函数
    • 二元信号量模拟互斥功能
  • 基于环形队列的生产者消费者模型
    • 空间资源和数据资源
    • 生产者和消费者使用资源
    • 两个规则
    • 代码实现
    • 信号量如何保护环形队列

信号量的概念

信号量的本质就是一个计数器 它描述着临界资源中资源数目的大小

  • 临界资源:多线程执行流共享的资源叫做临界资源

拿我们现实生活中的电影院举例 一个电影院中的座位是固定的 我们这里以100个为例

假设在一场电影开场前 我们就在手机app买了一张票 那么是不是只有我们到了电影院才有一个位置属于我呢?

显然不是 我们买完票的时候就获得了一个座位从电影开场到结束的使用权 此时电影院剩余的位置就变成了99个

而信号量的作用就是计数 如果电影院剩余位置是大于0的 就接受预定 如果不是就不接受预定

我们可以将电影院的所有座位看成是一份临界资源 而单个的座位则是这个临界资源的一小份 将一个个的人看作是线程的话 那么实际上我们就是在对这份临界资源实现并发操作 而信号量则是保证临界资源安全 不会被多预定造成异常的

信号量的使用

每个执行流想要使用临界资源之前都要申请信号量 每个执行流使用完临界资源之后都要释放信号量
在这里插入图片描述

信号量的PV操作:

  • P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一
  • V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。

因为信号量要被多个执行流访问并申请 所以说它实际上也是一个临界资源 我们也需要对它进行加锁操作 所以说申请和释放信号量的过程必须加锁

并且当信号量为0时 申请信号量的执行流将被挂起到信号量的等待队列中 直到有信号量被释放才被唤醒 所以说虽然信号量的本质是计数器 但是信号量中却不止有计数器 还有等待队列的等

信号量函数

初始化信号量

初始化信号量的函数叫做sem_init,该函数的函数原型如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明:

  • sem:需要初始化的信号量。
  • pshared:传入0值表示线程间共享,传入非零值表示进程间共享。
  • value:信号量的初始值(计数器的初始值)。

返回值说明:

  • 初始化信号量成功返回0,失败返回-1。

销毁信号量

销毁信号量的函数叫做sem_destroy,该函数的函数原型如下:

int sem_destroy(sem_t *sem);

参数说明:

  • sem:需要销毁的信号量。

返回值说明:

  • 销毁信号量成功返回0,失败返回-1。

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

等待信号量的函数叫做sem_wait,该函数的函数原型如下:

int sem_wait(sem_t *sem);

参数说明:

  • sem:需要等待的信号量。

返回值说明:

  • 等待信号量成功返回0,信号量的值减一。
  • 等待信号量失败返回-1,信号量的值保持不变。

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

发布信号量的函数叫做sem_post,该函数的函数原型如下:

int sem_post(sem_t *sem);

参数说明:

  • sem:需要发布的信号量。

返回值说明:

  • 发布信号量成功返回0,信号量的值加一。
  • 发布信号量失败返回-1,信号量的值保持不变。

二元信号量模拟互斥功能

当信号量为1时 此时信号量的作用便可以等价于锁

p操作为加锁 v操作为解锁

下面是一段不加锁的抢票操作

#include <iostream>                                                                                                                                                                                                                                           
#include <unistd.h>    
#include <pthread.h>    
#include <string>    
using namespace std;    
    
int ticket = 1000;    
    
void* TicketGrabbing(void* arg)    
{    
  string name = (char *)arg;    
  while(1)    
  {    
    if (ticket > 0)    
    {    
      usleep(1000);    
      cout << name << ": get a ticket ticket left : " << --ticket << endl;    
    }    
    else    
    {    
      break;    
    }    
  }    
    
  cout << "there is no ticket" << endl;    
  pthread_exit((void *)0);    
}    
    
    
    
int main()    
{    
  pthread_t tid1 , tid2 , tid3 , tid4;    
  pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");    
  pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");    
  pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");    
  pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");    
    
  pthread_join(tid1, nullptr);    
  pthread_join(tid2, nullptr);    
  pthread_join(tid3, nullptr);    
  pthread_join(tid4, nullptr);    
  return 0;    
}    

注意 usleep必须放在if里面才能大概率出现负数票

之后编译运行我们发现这种情况

在这里插入图片描述

下面我们在抢票逻辑中加上二元信号量 让每个执行流访问临界资源的时候首先申请信号量 访问完毕释放信号量

信号量部分代码如下

class SEM    
{    
  private:    
    sem_t _sem;    
  public:    
    SEM(int num = 1)    
    {    
      sem_init(&_sem , 0 , num);    
    }    
    
    ~SEM()    
    {    
      sem_destroy(&_sem);    
    }                                                                                                                                               
    
    void P()    
    {    
      sem_wait(&_sem);    
    }    
    
    void V()    
    {    
      sem_post(&_sem);    
    }
};

之后我们只要定义一个sem对象 像使用锁一样使用它就能得到我们想要的结果

注意: 申请锁(二元信号量)的过程必须要在if之前操作 因为进入if之后申请最后别的资源释放信号量之后一定会运行下面的代码

在这里插入图片描述

我们发现这里的票数就是0了

那么这里为什么一直是2线程在抢票呢? 因为此时2线程处于活跃状态 竞争能力比较强 所以说一直是它申请到信号量

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

在这里插入图片描述

空间资源和数据资源

对于生产者和消费者来说 它们关注的资源是不同的

  • 生产者关注的是队列中是否还有空间 只要有空间就能进行生产
  • 消费者关注的是队列中是否还有数据 只有有数据就能进行消费

初始空间设置

我们使用两个信号量来分别设置生产者和消费者的空间 对于这个队列来说 它一开始是空的 没有数据

所以对于生产者来说 它的信号量就是空间大小 对于消费者来说 它的信号量大小就是0

生产者和消费者使用资源

对于生产者

对于生产者来说它每次申请资源需要申请空间资源(P操作) 但是生产数据完毕之后不是对于空间资源进行V操作而是对于数据资源进行V操作 这是因为申请完数据之后整个环形队列中就会多一个数据资源而不是空间资源

对于消费者

对于消费者同理 当消费者消费一个数据之后整个环形队列中会多一个空间资源 而数据资源不会变化

两个规则

生产者和消费者不能同时访问一个位置

在这里插入图片描述

  • 如果生产者和消费者同时访问一个位置 相当于它们对于同一块临界资源进行了访问 这是不被允许的
  • 当它们同时访问不同位置的时候就可以同时进行生产和消费 没有冲突

无论是生产者和消费者 都不能将对方套一个圈以上

如果是生产者将消费者套一个圈以上的话 消费者上一圈的数据都没有消费完就被覆盖了 相当于是一些数据丢失了

如果是消费者将生产者套了一个圈以上的话 生产者还没有来得及生产数据 就会产生一些异常情况

代码实现

我们STL中的vector库来实现一个环形队列

W>  1 #pragma once
    2 
    3 #include <iostream>
    4 using namespace std;
    5 #include <unistd.h>
    6 #include <pthread.h>
    7 #include <string>
    8 #include <vector>
    9 #include <semaphore.h>
   10 
   11 const int CAP = 8;
   12 
   13 
   14 template<class T>
   15 class RingQueue
   16 {
   17   private:
   18     vector<T> _q;
   19     int _cap; // 容量大小
   20     int _p_pos; // 生产者位置
   21     int _c_pos; // 消费者位置 
   22     sem_t _blank_sem; // 空间信号量
   23     sem_t _data_sem; // 数据信号量 
   24   private:
   25     void P(sem_t& s)
   26     {
   27       sem_wait(&s);                                                                                                                
   28     }
   29     void V(sem_t& s)
   30     {
   31       sem_post(&s);
   32     }
   33   public:
W> 34     RingQueue(int num = CAP)
   35       :_cap(CAP),
   36       _p_pos(0),
   37       _c_pos(0)
   38     {
   39       _q.resize(CAP);                                                                                                              
   40       sem_init(&_blank_sem , 0 , _cap);
   41       sem_init(&_data_sem , 0 , _cap);
   42     }
   43 
   44     ~RingQueue()
   45     {
   46       sem_destroy(&_blank_sem);
   47       sem_destroy(&_data_sem);
   48     }
   49 
   50     void Push(const T& data)
   51     {
   52       this->P(_blank_sem);
   53       _q[_p_pos] = data;
   54       this->V(_data_sem);
   55 
   56       // 下一次生产的位置
   57       _p_pos++;
   58       _p_pos %= _cap;
   59     }
   60 
   61     void Pop(T& data)
   62     {
   63       this->P(_data_sem);
   64       data = _q[_c_pos];
   65       this->V(_blank_sem);
   66 
   67       // 下一次消费的位置
   68       _c_pos++;
   69       _c_pos %= _cap;
   70     }
   71 
   72 };

之后我们再写一个main函数 开始运行

  1 #include "RingQueue.hpp"
  2 
  3 void* P_run(void* args)
  4 {
  5   RingQueue<int>* rq = (RingQueue<int>*)args;
  6   cout << "im producer" << endl;
  7   while(1)
  8   {
  9     int data = rand()%100+1;                                                                                                         
 10     rq->Push(data);
 11     cout << "producer :" << data << endl;
 12   }
 13 }
 14 
 15 void* C_run(void* args)
 16 {
 17   auto rq = (RingQueue<int>*)args;
 18   sleep(3);
 19   cout << "im consumer" << endl;
 20   while(1)
 21   {
 22     sleep(1);
 23     int data = 0;
 24     rq->Pop(data);
 25     cout << "consumer :" << data << endl;
 26   }
 27 }
 28 
 29 
 30 int main()
 31 {
 32   pthread_t p;
 33   pthread_t c;
 34   srand((unsigned int)time(nullptr));
 35   auto* rq = new RingQueue<int>;
 36   pthread_create(&p , nullptr , P_run , rq);
 37   pthread_create(&c , nullptr , C_run , rq);
 38 
 39   pthread_join(p , nullptr);
 40   pthread_join(c , nullptr);
 41   delete rq;
 42   return 0;
 43 }

解释下上面的main函数

我们创造了两个线程 一个作为生产者线程 一个作为消费者线程 他们共用一个环形队列

首先生产者开始生产数据三秒 消费者休眠三秒

之后消费者每休眠一秒消费一个数据

当信号量满的时候 生产者就会阻塞在那里 直到消费者消费数据 生产者才会再生产

运行代码如下

在这里插入图片描述

同时我们还可以发现 消费数据和生产数据的顺序是一样的 这就是因为环形队列的缘故

信号量如何保护环形队列

在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。

只有当生产者和消费者指向环形队列的同一个位置的时候才会出现数据不一致的问题 而只有两种情况会让生产者和消费者指向同一个位置

  • 队列为空
  • 队列为满

而当队列为空的时候消费者是不能消费的

当队列为满的时候生产者是不能生产的

而除了这两种情况外其他所有情况生产者和消费者都是不能指向同一位置的 所以说信号量保护了环形队列的数据一致性问题

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

23-Ajax-axios

一、原生Ajax <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width…

中科大ChatGPT学术镜像小白部署教程,全民都可以拥抱AI

docker…不会用…python不会用…服务器默认python版本3.6不会升级…代理也不会配置…各种命令不会用… 那么下面就是最简单办法&#xff0c;点点点即可【希望有帮助&#xff1f;】 文章目录一、体验镜像地址二、 基本配置2.1 config.py文件2.2 main.py文件三、下载项目四、项目…

【C++】哈希表:开散列和闭散列

&#x1f4dd; 个人主页 &#xff1a;超人不会飞)&#x1f4d1; 本文收录专栏&#xff1a;《C的修行之路》&#x1f4ad; 如果本文对您有帮助&#xff0c;不妨点赞、收藏、关注支持博主&#xff0c;我们一起进步&#xff0c;共同成长&#xff01; 目录前言一、基于哈希表的两个…

一条更新语句的执行流程又是怎样的呢?

当一个表上有更新的时候&#xff0c;跟这个表有关的查询缓存会失效&#xff0c;所以这条语句就会把表T上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。 接下来&#xff0c;分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用ID这个索引。然…

JAVA+SQL离散数学题库管理系统的设计与开发

题库、试卷建设是教学活动的重要组成部分&#xff0c;传统手工编制的试卷经常出现内容雷同、知识点不合理以及笔误、印刷错误等情况。为了实现离散数学题库管理的信息化而开发了离散数学题库管理系统。 该系统采用C/S 模式&#xff0c;前台采用JAVA&#xff08;JBuilder2006&am…

如何选择合适的网络自动化工具

通过网络自动化工具实现网络自动化是所有网络组织的关键。如果没有合适的网络自动化工具&#xff0c;拥有由许多设备组成的大型网络环境的组织将无法执行重要操作&#xff0c;例如按时备份配置、实时跟踪不需要的更改以及遵守行业法规。当组织未能使用正确的网络自动化工具来执…

四百左右哪款蓝牙耳机比较好?400元价位蓝牙耳机推荐

除了日常通勤以及休息前听歌以外&#xff0c;随着加班变得频繁&#xff0c;工作时也戴起了耳机&#xff0c;由于市面上的耳机种类繁多&#xff0c;因此许多人不知道从而选择&#xff0c;小编发现更多的人是追求性价比&#xff0c;所以整理了一期四百左右性能表现优异的款式给大…

量化择时——LSTM深度学习量化择时(第1部分—因子测算)

之前我们尝试使用SVM&#xff0c;将时序数据转为横截面的数据&#xff0c;使用机器学习的方法进行预测 量化择时——SVM机器学习量化择时&#xff08;第1部分—因子测算&#xff09;&#xff1a; https://blog.csdn.net/weixin_35757704/article/details/129909497 但是因为股…

DHCP及中继(UOS)

DHCP服务器 中继器 客户端 服务器 安装DHCP apt install isc-dhcp-server -y 编辑配置文件 vim /etc/dhcp/dhcpd.conf 重启服务 systemctl restart isc-dhcp-server 配置监听网卡 vim /etc/default/isc-dhcp-server 中继器 安装dhcp yum install dhcp -y nmtui 修改…

pytest测试报告Allure - 动态生成标题生成功能、添加用例失败截图

一、动态生成标题 默认 allure 报告上的测试用例标题不设置就是用例名称&#xff0c;其可读性不高&#xff1b;当结合 pytest.mark.parametrize 参数化完成数据驱动时&#xff0c;如标题写死&#xff0c;其可读性也不高。 那如果希望标题可以动态的生成&#xff0c;采取的方案…

Hadoop 生态圈及核心组件简介Hadoop|MapRedece|Yarn

文章目录大数据时代HadoopHadoop概述Hadoop特性优点Hadoop国内外应用Hadoop发行版本Hadoop集群整体概述HDFS分布式文件系统传统常见的文件系统数据和元数据HDFS核心属性HDFS简介HDFS shell操作Map Reduce分而治之理解MapReduce思想分布式计算概念MapReduce介绍MapReduce产生背景…

[STM32F103C8T6]DMA

DMA(Direct Memory Access&#xff0c;直接存储器访问) 提供在外设与内存、存储器和存储器、外设 与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通&#xff0c;而不需要依赖于 CPU&#xff0c;在这个时间中&#xff0c;CPU对于内存的工作来说就无法使用。 我自己…

JDBC概述三(批处理+事务操作+数据库连接池)

一&#xff08;批处理&#xff09; 1.1 批处理简介 批处理&#xff0c;简而言之就是一次性执行多条SQL语句&#xff0c;在一定程度上可以提升执行SQL语句的速率。批处理可以通过使用Java的Statement和PreparedStatement来完成&#xff0c;因为这两个语句提供了用于处理批处理…

BGP策略实验

实验要求&#xff1a; 1、使用PreVa1策略&#xff0c;确保R4通过R2到达192.168.10.0/24 2、使用AS_Path策略&#xff0c;确保R4通过R3到达192.168.11.0/24 3、配置MED策略&#xff0c;确保R4通过R3到达192.168.12.0/24 4、使用Local Preference策略&#xff0c;确保R1通过R2到…

公司新招了个腾讯拿38K的人,让我见识到了什么才是测试天花板···

5年测试&#xff0c;应该是能达到资深测试的水准&#xff0c;即不仅能熟练地开发业务&#xff0c;而且还能熟悉项目开发&#xff0c;测试&#xff0c;调试和发布的流程&#xff0c;而且还应该能全面掌握数据库等方面的技能&#xff0c;如果技能再高些的话&#xff0c;甚至熟悉分…

【失业即将到来?】AI时代会带来失业潮吗?

文章目录前言一、全面拥抱AIGC二、AI正在取代这类行业总结前言 兄弟姐妹们啊&#xff0c;AI时代&#xff0c;说抛弃就抛弃&#xff0c;真的要失业了。 一、全面拥抱AIGC 蓝色光标全面暂停外包&#xff1f; 一份文件截图显示&#xff0c;中国知名4A广告公司&#xff0c;蓝色光…

【GPT4】微软 GPT-4 测试报告(5)与外界环境的交互能力

欢迎关注【youcans的AGI学习笔记】原创作品 微软 GPT-4 测试报告&#xff08;1&#xff09;总体介绍 微软 GPT-4 测试报告&#xff08;2&#xff09;多模态与跨学科能力 微软 GPT-4 测试报告&#xff08;3&#xff09;编程能力 微软 GPT-4 测试报告&#xff08;4&#xff09;数…

基于AI分词模型,构建一个简陋的Web应用

文章目录前言1. 效果展示2. 应用设计3. 实现3.1. lac分词模型的服务化部署3.2 使用Flask构建app4. 小结前言 内容纯属个人经验&#xff0c;若有不当或错误之处&#xff0c;还请见谅&#xff0c;欢迎指出。 文中大致介绍了&#xff0c;如何快捷地使用PaddleHub服务化部署一个简…

九龙证券|昨夜,大涨!蔚来5.99%,小鹏15.22%,理想6.39%

当地时间周一&#xff0c;美股三大指数低开高走&#xff0c;尾盘小幅收涨。盘面上&#xff0c;银行股、航空股遍及上涨。 展望本周&#xff0c;包括美联储理事沃勒、鲍曼等官员将迎来下月会议沉默期前的最终说话&#xff0c;投资者需关注其对经济和货币政策前景的看法。此外&am…

如何在TikTok视频描述中提高用户参与度

鑫优尚电子商务&#xff1a;TikTok视频描述&#xff08;包括话题标签&#xff09;有150个字符的限制&#xff0c;因此卖家需要合理撰写出有趣且有实际意义的视频描述。可尝试将描述保持在140个字符以内&#xff0c;将最重要的信息放在前面&#xff0c;并通过多次修改文案以排除…