【Linux】-同步互斥的另一种办法-信号量

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、信号量的概念
    • 二、POSIX信号量
  • 三、总结


前言

今天我们来讲解一下信号量,相比较之前学习的多线程中的互斥锁来说,信号量的概念比互斥锁要难理解,但是博主会使用生活中的例子,来给大家讲解,最后会得出互斥锁其实是信号量的一种特殊情况,但不是信号量,而理解的本质却是一样的,最后博主会使用代码给大家演示信号量是怎么实现多线程的同步互斥的


一、信号量的概念

为什么会有信号量,他也是解决我们多线程访问共享资源的安全问题,之前的锁已经帮助我们解决了这个问题,那为什么还需要信号量呢??计算机中任何一种东西的产生都有他适用的场景,所以信号量的产生是为了适用于其他场景的。我们的锁他的作用是将一整块共享资源都保护起来,让一个执行流去访问,万一这个执行流只访问这块共享资源的一部分,那么就没有必要把整块都保护起来,只需要保护他要访问共享资源的一部分就可以了,所以我们就可以将这块共享资源分成许多份,而信号量就是表示分成多少份的数量信号量(信号灯)的本质就是一把计算器,类似于int cnt=n(不是等于),用于描述临界资源数量的多少。

讲一个故事:

作为大学生,常见的娱乐就是看电影,电影院会在电影开始前,会卖票(票就是我们所以人的共享资源),假设电影院有100个座位,那么商家就会放出100张不重复的票(信号量就是100,表示临界资源有100个),他不会放多,也不会放少。此时我们就可以得出下面三个特点:

  1. 当我们看电影的时候,我们还没有去电影院,先买票–买票的本质就是对资源的预定机制(就是申请信号量的过程)
  2. 当我们买了一张票的时候,计数器就会减1,资源就是少1
  3. 当计数器到0之后,资源已经被申请完毕了

在这里插入图片描述

相比较之前的锁是对整个临界资源进行保护,而信号量则是对整体里面的一个小块进行预订,当多个执行流过来访问这个临界资源,必须先申请信号量,申请成功了这块资源就是自己的了,所以此时我们最怕的是:1.多个执行流访问一个小块的资源。2.n个资源,但有N+条执行流的时候必然会造成前面一点,所以信号量的作用机来了,信号量的数目就是资源数量,而执行流想要访问资源就必须申请信号量成功,此时信号量就会减1,当你使用资源的时候,就要释放信号量,此时信号量加1,这也是信号量的工作机制,也可以很好的解决第二点,对于第一点,如果有多个执行流来访问同一个资源,就可以把这个小块当成整体,在这个小块的临界资源上加锁,来实现互斥,对于这一整块临界资源,不就可以用一个剩余票数信号量表示,也可以设置一个卖出票数信号量表示,这样两个信号量自己申请信号量,释放对方的信号量,这个一会在案例当中会非常明显。 通过上面描述得出下面四点结论:

  1. 申请计数器成功,就表示我具有访问资源的权限了,就好比买到票了,座位就是我的,不管我去不去。
  2. 申请了计数器资源,我当前访问我要的资源了吗??没有。申请了计数器资源就是对资源的余地给机制。
  3. 计数器可以有效保证进入共享资源的执行流数量
  4. 所以每一个执行流,想要访问共享资源的一部分的时候,不是直接访问,而是先申请计数器资源

在此理解:
如果我们电影院里面只有一个座位呢??说明我们的临界资源数目就不是之前的100,而是1,所以信号量的数目就是1,只有一个人能抢到票,只有一个人可以看电影,也就是看电影期间只有一个执行流在访问临界资源,这个在之前说过的,当临界资源只能有一个执行流去访问这不就是之前说的线程互斥嘛。我们把信号量的值只能为1,0两态的计数器叫做二元信号量----本质就是锁
所以博主一开始说什么锁其实是信号量的一个特殊情况,但不是一个动心,本质理解是一样的。

其实让资源为1,这是程序员规定的,假设资源有100份,你也不可能设置成90份,也不可能设置多,设置为1,表示这块资源只能分成一份,不要分成多份,而是当成一个整体,整体申请,整体释放----整体加锁,整体解锁。

思考一下:
想要访问临界资源的前提是先申请信号量计数器资源,那么信号量计数器不也是共享资源嘛??当我们申请或者释放信号量的时候,要进行–和++,这个操作在多线程知识张杰说过不是原子的,所以在多线程的时候,可能在某一条汇编的时候就别切换走了,导致申请信号量出现问题。

申请信号量,本质是对计数器–,叫做P操作
释放信号量,本质是对计数器++,叫做V操作
正常++和–都不是原子性的,但是PV操作必须是原子的,只有一条汇编才是原子的(要么做,要么不做,没有正在做)

概念的总结:

  1. 信号量本质是一把计数器,PV操作,原子的。
  2. 执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源
  3. 信号量值1,0两态的叫做二元信号量,就是互斥锁功能
  4. 申请信号量的本质:是对临界资源的预定机制。

二、POSIX信号量

上面做了很多的铺垫,接下来我们要实现就是把之前的CP模型改成用信号量去实现的模式,我们之前的cp模型是基于阻塞队列去实现的,而现在我们是基于环形队列的cp模型,因为我们将一个整体分成许多块了,所以整个临界资源就不止一个线程在访问,所以需要下标来判断生产者和消费者线程的位置,从而使用下标的方式让他们不会访问同一个小块。之前我们学习过的环形队列,其实就是使用数组,然后对下标进行模运算,间接实现环形的效果。

在这里插入图片描述
来看代码实现,在CP模型的时候,写了一个任务计算器,当时没有演示,现在拿来用用:
RingQueue.hpp:

#include<iostream>
#include<semaphore.h>
#include<vector>
using namespace std;



template<class T>
class RingQueue
{
    
public:  
   
    RingQueue(int cap):cap_(cap),rq_(cap)
    {
        sem_init(&p_sem_,0,cap);//生产者一开始的信号量数目是cap
        sem_init(&c_sem_,0,0);//消费者一开始的信号量数目是0

        pthread_mutex_init(&lock_,NULL);//给锁进行初始化。

        p_index_=0;
        c_index_=0;
    }

     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);
    }
    void push(T data)
    {
        P(&p_sem_);
        lock(&lock_);
        rq_[p_index_]=data;
        p_index_++;
        p_index_=p_index_%cap_;
        unlock(&lock_);
        V(&c_sem_);
    }
    void pop(T *data)
    {
        P(&c_sem_);
        lock(&lock_);
        *data=rq_[c_index_];
        c_index_++;
        c_index_=c_index_%cap_;
        unlock(&lock_);
        V(&p_sem_);
    }
    ~RingQueue()
    {
        sem_destroy(&p_sem_);
        sem_destroy(&c_sem_);
        pthread_mutex_destroy(&lock_);
    }
private:
  vector<T> rq_;
  sem_t p_sem_;
  sem_t c_sem_;

  int p_index_;//生产者下标用来表示生产者到哪一块资源了
  int c_index_;

  pthread_mutex_t lock_;

  int cap_;//表示环形队列的最大长度
};

main.cc:

#include"RingQueue.hpp"
#include"task.hpp"
#include<string>
#include<unistd.h>
#include<ctime>
template<class T>
struct RQInfo
{
    RingQueue<T>* rq;
    string name;
};
void *producer_fun(void *arg)
{
    RQInfo<Task>* rq = static_cast<RQInfo<Task>*>(arg);
    string name = rq->name;
    RingQueue<Task>* rq_ptr = rq->rq;

    while(true)
    {
        //生产任务
        int x=rand()%10+1;
        int y=rand()%10;
        char op=opers[rand()%opers.size()];
        Task t(x,y,op);
        rq_ptr->push(t);

        cout<<name<<" produce a task: "<<t.GetTask()<<endl;
        sleep(1);
    }
    return nullptr;
}

void *consumer_fun(void *arg)
{
    RQInfo<Task>* rq = static_cast<RQInfo<Task>*>(arg);
    string name = rq->name;
    RingQueue<Task>* rq_ptr = rq->rq;
    while(true)
    {
        Task t;
        rq_ptr->pop(&t);
        t();
        cout<<name<<" consume a task: "<<t.GetResult()<<endl;
    }
    return nullptr;
}
int main()
{
    srand(time(NULL));
    pthread_t producer[3],consumer[3];
    RQInfo<Task>* rq_info=new RQInfo<Task>();
    RingQueue<Task> *rq = new RingQueue<Task>(10);
    rq_info->rq = rq;
    //单生产单消费的环形CP模型。

    for(int i=0;i<1;i++)
    {
        rq_info->name = "productor-"+to_string(i);
        pthread_create(producer+i,NULL,producer_fun,rq_info);
        sleep(1);
        
    }
    for(int i=0;i<1;i++)
    {
        rq_info->name = "consumer-"+to_string(i);
        pthread_create(consumer+i,NULL,consumer_fun,rq_info);
        sleep(1);
    }

    for(auto tid:producer)
    {
        pthread_join(tid,NULL);
    }

    for(auto tid:consumer)
    {
        pthread_join(tid,NULL);
    }
    return 0;
}

测试办法:

  1. 先在main.c中将生产者消费者设置成单生产单消费,然后使生产者先休眠三秒,看看消费者什么状态,目的是测试生产者是不是先跑
    在这里插入图片描述
    代码设计细节:
    1.通过结构体将线程名和环形队列一起当参数传进去
    2.在设计一些接口时可以进行封装,我们信号量的借口和我们熟悉的PV操作不对应,封装成熟悉的
    3.我们进行加锁和申请信号量的时候,有两种一种先加锁,一种先申请,哪种好??答案是先申请好,(1)信号量本身就是原子的,不需要被保护。(2)先加锁在申请是一个串行,如果先申请在加锁,就会出现一个线程在访问的时候,其他线程可以申请信号量,实现并发访问。

三、总结

如果没有学习多线程,听信号量是一件痛苦的事情,但是我们学过多线程并且还学了锁,对于信号量的理解我认为大家是没有问题的,大家下来要去联系一下代码,多去测试一些情况,看看和自己预想的是不是一样的。话不多了,这篇就讲到这里,下篇我们开始讲解线程池,希望大家到时侯过来支持。

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

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

相关文章

身份证也可以cisa远程考试

CISA CISM CRISC CGEIT ​只有身份证 ​没有护照 ​没有港澳通行证 ​也可以线上考试

python学习20

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

object detection的一些pre trained模型(视频可以实现一下)

https://www.youtube.com/watch?v2yQqg_mXuPQ 你的支持是我创作的源泉

CC++内存管理【非常详细,对新手友好】

文章目录 一、程序内存划分1.基础知识2. 堆栈的区别3. 题目练手 二、C语言中动态内存管理方式三、C中动态内存管理方式1. new/delete操作内置类型2. new/delete操作自定义类型 四、operator new和operator delete函数1. 汇编查看编译器底层调用2. 透过源码分析两个全局函数 五、…

老旧小区火灾频发,LoRa无线系统筑牢安全防线

近日&#xff0c;全国各地多个老旧小区火灾事故频发&#xff0c;从安微合肥南二环一老旧小区居民楼起火、上海金山区一小区居民楼火灾&#xff0c;到1月24日江西新余市特大火灾......都造成了不同程度的人员伤亡和财产损失&#xff0c;令人扼腕痛惜&#xff0c;教训十分深刻。 …

浅谈 ts的类型校验 经验分享

经验1&#xff1a; 【input"testVal $event.target.value"】会有一个ts报错&#xff1a;【“$event.target”可能为 “null”。】我们可以使用【input"testVal (<HTMLInputElement>$event.target).value"】解决ts报错<input type"text&quo…

C#-前后端分离连接mysql数据库封装接口

C#是世界上最好的语言 新建项目 如下图所示选择框红的项目 然后新建 文件夹 Common 并新建类文件 名字任意 文件内容如下 因为要连接的是mysql数据库 所以需要安装 MySql.Data.MySqlClient 依赖; using MySql.Data.MySqlClient; using System.Data;namespace WebApplication1.…

【Image captioning】论文阅读八—ClipCap: CLIP Prefix for Image Captioning_2021

中文标题&#xff1a;ClipCap: CLIP前缀用于图像描述&#xff08;ClipCap: CLIP Prefix for Image Captioning&#xff09; 文章目录 1. 介绍2. 相关工作3. 方法3.1 综述3.2 语言模型微调3.3 映射网络架构3.4 推理 4. 结果5. 结论 摘要&#xff1a;图像描述是视觉语言理解中的…

黑群晖屏蔽更新

黑群晖屏蔽更新 修改Host删除控制面板的红点和更新提示 修改Host ssh连接群晖后执行以下命令 sudo vim /etc/hosts按i键进入编辑模式 光标移动定位到最后一行后追加以下两行 127.0.0.1 update.synology.com 127.0.0.1 update7.synology.com按esc键&#xff0c;然后输入:wq并…

Nginx进阶篇【四】

Nginx进阶篇【四】 六、Nginx负载均衡6.1.负载均衡概述6.2.负载均衡的原理及处理流程6.3.负载均衡的作用6.4.负载均衡常用的处理方式6.4.1.方式一:用户手动选择6.4.2.方式二:DNS轮询方式6.4.2.1.DNS6.4.2.2.为某一个域名添加的IP地址&#xff0c;用2台服务器来做负载均衡6.4.2.…

ROS2学习笔记(0)开坑声明

0.前提 在做racecar的过程中发现已经有不少的开发者和公司开始从ros1转向ros2的怀抱了&#xff0c;刚好寒假在家&#xff0c;我就顺带试试看能不能学点ros2&#xff0c;刚好我有两辆车和主板可以双线开工&#xff08;是的&#xff0c;全是老师们赞助的&#xff0c;真的我哭死&…

Java面试题之序列化和反序列化

Java面试题之序列化和反序列化 文章目录 Java面试题之序列化和反序列化序列化和反序列化什么是序列化?什么是反序列化?如果有些字段不想进行序列化怎么办&#xff1f;常见序列化协议有哪些&#xff1f;为什么不推荐使用 JDK 自带的序列化&#xff1f; 文章来自Java Guide 用于…

Python初学者学习记录——python基础综合案例:数据可视化——地图可视化

一、基础地图使用 1、基础地图演示 2、基础地图演示——视觉映射器 from pyecharts.charts import Map from pyecharts.options import VisualMapOpts# 准备地图对象 map Map() # 准备数据 data [("北京市", 99),("上海市", 199),("湖南省", 2…

stable-diffusion-webui 汉化(中文界面)

大家好&#xff0c;我是水滴~~ 本文主要介绍 Stable Diffusion WebUI 是如何汉化的&#xff0c;文章详细的介绍汉化过程&#xff0c;并加上配图能够清晰的展示该过程。 Stable Diffusion WebUI 官方并没有出中文界面&#xff0c;需要通过安装插件来汉化&#xff0c;下面是详细…

MySQL-进阶-SQL优化

一、insert优化 插入大量数据 二、主键优化 1、数据组织方式 2、页分裂 3、页合并 4、逐渐设计原则 三、order by优化 四、group by优化 五、limit优化 六、count优化 七、update优化

3 JS类型 值和变量

计算机对value进行操作。 value有不同的类型。每种语言都有其自身的类型集合。编程语言的类型集是该编程语言的基本特性。 value需要保存一个变量中。 变量的工作机制是变成语言的另一个基本特性。 3.1概述和定义 JS类型分为&#xff1a; 原始类型和对象类型。 原始类型&am…

react实现滚动到顶部组件

新建ScrollToTop.js import React, { useState, useEffect } from react; import ./ScrollToTop.css;function ScrollToTop() {const [isVisible, setIsVisible] useState(true);// Show button when page is scorlled upto given distanceconst toggleVisibility () > {…

【K12】运用tk控件演示欧姆定律串联电阻小应用

上述代码是一个基于Python的图形用户界面&#xff08;GUI&#xff09;应用程序&#xff0c;用于演示欧姆定律。用户可以通过输入电阻值来计算电流&#xff0c;并在图形上显示结果。该程序使用了Tkinter库来创建GUI&#xff0c;matplotlib库来绘制图形&#xff0c;以及numpy库进…

docker 构建应用

docker 应用程序开发手册 开发 docker 镜像 Dockerfile 非常容易定义镜像内容由一系列指令和参数构成的脚本文件每一条指令构建一层一个 Dockerfile 文件包含了构建镜像的一套完整指令指令不区分大小写&#xff0c;但是一般建议都是大写从头到尾按顺序执行指令必须以 FROM 指…

大型语言模型 (LLM)全解读

一、大型语言模型&#xff08;Large Language Model&#xff09;定义 大型语言模型 是一种深度学习算法&#xff0c;可以执行各种自然语言处理 (NLP) 任务。 大型语言模型底层使用多个转换器模型&#xff0c; 底层转换器是一组神经网络。 大型语言模型是使用海量数据集进行训练…