Liunx系统编程:信号量

一. 信号量概述

1.1 信号量的概念

在多线程场景下,我们经常会提到临界区和临界资源的概念,如果临界区资源同时有多个执行流进入,那么在多线程下就容易引发线程安全问题。

为了保证线程安全,互斥被引入,互斥可以保证在同一时刻只有一个执行流进入临界区访问临界资源,由于整个临界区都只允许一个执行流进入,我们可以认为互斥是将临界区当做一个整体来使用的

但是,如图1.1,假设下面这种场景,一个临界区资源被分为N个小区域,每个小区域都有特定的数据,如果多个执行流同时访问同一个小区域,那么线程之间就会相互干扰,存在线程不安全问题,但如果多个执行流在某一时刻访问不同的小区域,保证每个小区域在同一时刻不会有多个执行流访问,那么即使有多个执行流进入临界区,也不存在线程安全问题。

结论:多个同时进入临界区的执行流,如果不访问同一块资源,就不会有线程不安全问题。

图1.1 多线程访问临界资源线程安全和不安全的场景

为了让多跟线程能同时访问临界资源,并且保证线程安全,信号量的概念被引入,以保证进入临界区的线程不访问同一块临界资源,以此来提高多线程的效率。

信号量的本质为计数器count,用于表示临界区还有多少资源。

当某个执行流要访问临界区资源前,要先申请信号量,计数器count--,申请信号量的操作被称为P操作,如果临界区内还有资源,那么申请信号量就会成功,计数器count--,拿到信号量之后,该线程执行流就拥有了进入临界区的权利。

申请到了信号量,本质是一种资源预定机制,并不是说申请到了信号量已经在访问临界资源了,但申请到了信号量的执行流具有访问临界资源的权利,可以在适当的时候访问临界资源。

当执行流访问完临界资源后,要释放信号量,计数器count++,释放信号量的操作被称为V操作,这样之前等待信号量的资源,就可以拿到信号量,以进入临界区访问临界资源。

至于申请到了信号量后访问的是那一块临界资源,信号量本身并无法指定,需要程序员编程决定。

结论:(1). 信号量本质为计数器,用于表示还剩多少临界资源  (2). 访问临界资源前要通过申请信号量来预定临界资源,信号量计数器--,称为P操作  (3). 离开临界区要释放信号量,信号量计数器++,被称为V操作  (4). 如果临界区内没有剩余资源,此时信号量为0,线程申请不到信号量就会被阻塞。 

这里借助生活中的场景,来辅助理解信号量。假设某明星演唱会现场观看的座位数为200,这200个现场座位为共享资源,每个座位就是临界区内的一小块资源。当现场观看的票还没有卖出时,剩余资源数为200,初始信号量为200。

如果此时有人买走了一张票,那么他就预定了一个现场座位,即预定一份共享资源,即使他不去现场观看,那么这个座位也属于他,其他人不能占用,预定一张票,就是申请一个信号量,计数器count--,由200变为199。

如果某时200个座位都被预定了,剩余资源就变为0,类似于信号量为0,此时再有人想预定现场座位,就无法预定成功,这与线程在信号量为0的时候无法预定临界资源类似。

如果演唱会结束,或某人退票,那么就释放了一个临界区资源,信号量计数器count++,这时座位就又可以预定了,类似于多线程中某一线程执行流离开临界区释放信号量,这个信号量就可以被之前因为信号量为0而被阻塞的线程拿到,进入临界区访问资源。

1.2 信号量相关函数

信号量的初始化:

  • 通过函数sem_init可初始化信号量。
  • 初始化信号量的时候,就应当指定初始值,即:有多少临界资源可以被不同执行流访问。

sem_init -- 信号量初始化函数

函数原型:sem_init(sem_t *sem, int pshared, unsigned int value);

头文件:#include <semaphore.h>

函数参数:

  • sem -- 被初始化的信号量的地址
  • pshared -- 0表示同一进程下的线程间共享,1表示进程间共享
  • value -- 信号量的初始值

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

信号量等待:

  • 通过sem_wait函数,可以让线程等待信号量。
  • 如果当前信号量不为0,线程申请(等待)到了信号量,那么这个线程就预定了一份临界资源,信号量计数器--。
  • 如果当前信号量为0,即没有剩余的临界资源了,线程就需要等待一份临界资源被释放,才能申请到信号量。
  • 申请信号量,调用sem_wait的操作,被称为P操作。

sem_wait函数 -- 申请(等待)信号量

函数原型:int sem_wait(sem_t *sem)

头文件:#include<semaphore.h>

函数参数:sem -- 被等待的信号的地址

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

信号量释放:

  • 通过sem_post函数可以实现释放信号量资源。
  • 如果某一线程申请到了信号量并访问了临界资源,访问临界资源完成后,要释放信号量,让其他正在等待信号量的线程可以拿到信号量并访问临界资源。
  • 释放信号量,信号量计数器++,这样的操作被称为V操作。

sem_post函数 -- 释放信号量

函数原型:int sem_post(sem_t *sem)

头文件:#include <semaphore.h>

函数参数:sem -- 被等待的信号的地址。

返回值:函数执行成功返回0,失败返-1并设置错误码

二. 通过环形队列实现生产与消费者模型

2.1 环形结构解析

图2.1为环形队列的逻辑结构和物理结构图,在其底层实现代码中,依旧是采用线性数组来实现的,只不过我们通过特定的计算机代码,来使其行为与首尾相连的环形结构一致。

图2.1 环形队列的物理结构和逻辑结构

假设环形队列能够容纳N个元素,那么我们在拿到下标为index的位置时,如要找到其后面第k个元素的位置,计算方法为:(index + k) % N。

有两种方法,可以判断环形队列是空还是满:

  • 用计数器来辅助:如果计数器count = 0,环形队列就是空,如果等同于环形队列的最大容量N,即count = N,就是满。
  • 间隔空位:相比于环形队列的最大容量,多开辟一个数据空间,采用两个指针first和last记录首个元素位置和末尾元素后面的位置,如果last == fisrt,那么环形队列为空,如果(last + 1) % N == first 成立,那么环形队列为满,图2.2为这种方法的。
图2.2 环形队列满和空的情况

2.2 生产消费者模型与环形队列的联系

如果采用阻塞队列的方式来实现生产与消费者模型,由于C++ STL中提供的queue不向用户暴露底层实现,并且将阻塞队列视为一个整体来进行数据的写入和读取,造成了某一时刻只允许一个生产者线程或一个消费者线程访问临界资源(阻塞队列),为了保证线程安全,生产者写数据和消费者读数据不能够同时进行。

如图2.3所示,假设我们希望向p_step所指向的位置写数据,从c_step所指向的位置读数据,由于p_step和c_step所指向的是环形队列的不同位置,此时生产者和消费者线程如果并发执行,不会出现线程不安全问题,因为这两个执行流访问的是临界资源的不同区域。

但是,如果p_step和c_step指向环形队列的同一位置,此时生产者线程和消费者线程并发执行,则会访问临界资源的相同区域,引发线程不安全问题。

允许一定条件下的生产者线程和消费者线程并发执行,可以显著降低等待时间,提高程序整体的运行效率。

图2.3 生产与消费者线程可以并发执行和不能并发执行的场景

3.3 基于环形队列的生产与消费者模型实现代码

在程序中,可以采用信号量的方式来决定是否让生产者线程或消费者线程阻塞等待,我们假设环形队列的最大容量为N,那么就定义两个信号量:

  • _sem_space:空间信号量,表示是否还有剩余空间,初值设为N。
  • _sem_data:数据信号量,表示是否还有可读数据资源,初值设为0。

当生产者要向环形队列中写数据时,要先申请空间信号量,如果申请空间信号量成功,说明环形队列中有剩余空间,才能向环形队列中写数据,当访问完临界资源后,要释放数据信号量,唤醒因阻塞队列中没有数据而等待数据信号量的消费者线程。

当消费者从环形队列中读取数据时,要先申请数据信号量,如果申请成功,说明环形队列中有可读数据,这时消费者线程才能够读取环形队列中的数据,当访问完临界资源后,要释放空间信号量,唤醒因环形队列没有空间而阻塞等待空间信号量的生产者线程。

虽然信号量也是临界资源,但是对信号量的++/--操作是原子的,所以不会存在线程不安全问题。

代码3.1:头文件Sem.hpp -- 封装信号量

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

// 封装用于操作信号量的类
class Sem
{
public:
    // 构造函数,实现初始化信号量
    Sem(int pshared, int value)
    {
        sem_init(&_sem, pshared, value);
    }

    // 析构函数,销毁信号量
    ~Sem()
    {
        sem_destroy(&_sem);
    }

    // 等待信号量 -- p操作
    void p()
    {
        sem_wait(&_sem);
    }

    // 释放信号量 -- v操作
    void v()
    {
        sem_post(&_sem);
    }

private:
    sem_t _sem;   // 信号量
};

代码3.2:头文件RingQueue.hpp -- 实现阻塞队列

#pragma once

#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"

int g_DFL_CAPACITY = 5;  // 信号量默认初值

template<class T>
class RingQueue
{
public:
    // 构造函数
    RingQueue(int capacity = g_DFL_CAPACITY)
        : _ring_queue(capacity, T())
        , _capacity(capacity)
        , _p_step(0)
        , _c_step(0)
        , _sem_data(0)
        , _sem_space(capacity)
    { 
        // 初始化生产者线程和消费者线程互斥锁
        pthread_mutex_init(&_c_mtx, nullptr);
        pthread_mutex_init(&_p_mtx, nullptr);
    }

    // 析构函数
    ~RingQueue()
    {
        // 销毁生产者线程和消费者线程互斥锁
        pthread_mutex_destroy(&_c_mtx);
        pthread_mutex_destroy(&_p_mtx);
    }

    // 生产者写数据函数
    void push(const T& val)
    {
        // 1. 申请空间信号量 -- p操作
        _sem_space.p();

        // 2. 加锁 -> 写数据 -> 解锁
        pthread_mutex_lock(&_p_mtx);   // 加锁
        _ring_queue[_p_step++] = val;  // 写数据
        _p_step %= _capacity;          // 更新下标
        pthread_mutex_unlock(&_p_mtx); // 解锁

        // 3. 释放数据信号量
        _sem_data.v();
    }

    // 消费者读数据函数,data为输出型参数
    void pop(T* data)
    {
        // 1. 申请数据信号量
        _sem_data.p();
        
        // 2. 加锁 -> 读数据 -> 解锁
        pthread_mutex_lock(&_c_mtx);   // 加锁
        *data = _ring_queue[_c_step++]; // 读数据
        _c_step %= _capacity;          // 更新下标
        pthread_mutex_unlock(&_c_mtx); // 解锁

        // 3. 释放空间信号量
        _sem_space.v();
    }

private:
    std::vector<T> _ring_queue; // 用线性表模拟实现的环形队列
    int _capacity;              // 环形队列容量
    int _p_step;                // 生产者向环形队列写数据的下标位置
    int _c_step;                // 消费者从环形队列中读取数据的下标
    pthread_mutex_t _c_mtx;     // 用于控制消费者线程的互斥锁
    pthread_mutex_t _p_mtx;     // 用于控制生产者线程的互斥锁
    Sem _sem_data;              // 用于表示环形队列中现有数据的信号量
    Sem _sem_space;             // 用于表示环形队列中剩余空间的信号量 
};

代码3.3:ConProd.cc文件 -- 生产消费者模型main函数所在源文件

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"

// 消费者线程入口函数
void* consume(void* args)
{
    RingQueue<int> *prq = (RingQueue<int>*)args;

    // 间隔1s从环形队列中读数据
    int data;
    while(true)
    {
        prq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
        sleep(1);
    }

    return nullptr;
}

void* product(void* args)
{
    RingQueue<int> *prq = (RingQueue<int>*)args;
    
    // 死循环向环形队列中写数据
    int a = 0;
    while(true)
    {
        std::cout << "生产数据:" << a << std::endl;
        prq->push(a);
        a++;
    }

    return nullptr;
}

int main()
{
    RingQueue<int> *prq = new RingQueue<int>();

    // 闯将两个生产者线程,三个消费者线程
    pthread_t p[2], c[3];
    pthread_create(p, nullptr, product, (void*)prq);
    pthread_create(p + 1, nullptr, product, (void*)prq);
    pthread_create(c, nullptr, consume, (void*)prq);
    pthread_create(c + 1, nullptr, consume, (void*)prq);
    pthread_create(c + 2, nullptr, consume, (void*)prq);

    // 阻塞等待生产者消费者线程退出
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(c[2], nullptr);

    return 0;
}

三. 总结

  • 信号量的本质为一计数器,用于表示临界区内还剩多少资源。
  • 通过使用信号量,可让多个线程执行流去访问临界资源的不同区域,达到某时刻多个执行流进入临界区,但不会造成线程不安全的目的。
  • 线程进入临界区前要先申请信号量,在离开临界区后要释放信号量。

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

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

相关文章

redis面试题二

redis如何处理已过期的元素 常见的过期策略 定时删除&#xff1a;给每个键值设置一个定时删除的事件&#xff0c;比如有一个key值今天5点过期&#xff0c;那么设置一个事件5点钟去执行&#xff0c;把它数据给删除掉&#xff08;优点&#xff1a;可以及时利用内存及时清除无效数…

华为Mate60低调发布,你所不知道的高调真相?

华为Mate60 pro 这两天的劲爆新闻想必各位早已知晓&#xff0c;那就是华为Mate60真的来了&#xff01;&#xff01;&#xff01;并且此款手机搭载了最新国产麒麟9000s芯片&#xff0c;该芯片重新定义了手机性能的巅峰。不仅在Geekbench测试中表现出色&#xff0c;还在实际应用…

CTFhub-SSRF-内网访问

CTFHub 环境实例 | 提示信息 http://challenge-8bf41c5c86a8c5f4.sandbox.ctfhub.com:10800/?url_ 根据提示&#xff0c;在url 后门添加 127.0.0.1/flag.php http://challenge-8bf41c5c86a8c5f4.sandbox.ctfhub.com:10800/?url127.0.0.1/flag.php ctfhub{a6bb51530c8f6be0…

基于深度学习的三维重建从入门实战教程 原理讲解 源码解析 实操教程课件下载

传统的重建方法是使用光度一致性等来计算稠密的三维信息。虽然这些方法在理想的Lambertian场景下,精度已经很高。 但传统的局限性,例如弱纹理,高反光和重复纹理等,使得重建困难或重建的结果不完整。 基于学习的方法可以引入比如镜面先验和反射先验等全局语义信息,使匹配…

elementui tree 层级过多时,高亮状态无法选满整行

问题&#xff1a; 如上图所示&#xff0c;官方的tree组件&#xff0c;在层级很多时 elementui -tree 的高亮状态并没有选中整行。 &#xff08;衍生库 vue-easy-tree 也会出现此问题&#xff09; 原因&#xff1a; &#xff08;没有查看源码&#xff0c;只是根据dom简单定位…

Echart笔记

Echart笔记 柱状图带背景色的柱状图将X与Y轴交换制作为进度条 柱状图 带背景色的柱状图 将X与Y轴交换制作为进度条 //将X与Y轴交换制作为进度条 option { xAxis: {type: value,min:0,max:100,show:false,//隐藏x轴},yAxis: {type: category,data:[进度条],show:false,//隐…

Citespace、vosviewer、R语言的文献计量学 、SCI

文献计量学是指用数学和统计学的方法&#xff0c;定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体&#xff0c;注重量化的综合性知识体系。特别是&#xff0c;信息可视化技术手段和方法的运用&#xff0c;可直观的展示主题的研究发展历程、研究现状、研究…

URL重定向漏洞

URL重定向漏洞 1. URL重定向1.1. 漏洞位置 2. URL重定向基础演示2.1. 查找漏洞2.1.1. 测试漏洞2.1.2. 加载完情况2.1.3. 验证漏洞2.1.4. 成功验证 2.2. 代码修改2.2.1. 用户端代码修改2.2.2. 攻击端代码修改 2.3. 利用思路2.3.1. 用户端2.3.1.1. 验证跳转 2.3.2. 攻击端2.3.2.1…

使用正则表达式在中英文之间添加空格

有时为了排版需要&#xff0c;我们可能需要在文章的中英文之间添加空格&#xff0c;特别是中文中引用了英文单词时&#xff0c;这种情况使用正则表达式整体修订是最明智的做法。首先&#xff0c;推荐使用在线的正则表格式工具&#xff1a;https://regex101.com/ , 该工具非常强…

LeetCode-53-最大子数组和-贪心算法

贪心算法理论基础&#xff1a; 局部最优推全局最优 贪心无套路~ 没有什么规律~ 重点&#xff1a;每个阶段的局部最优是什么&#xff1f; 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#…

煤矿监管电子封条算法

煤矿监管电子封条算法基于yolov5网络模型深度学习框架&#xff0c;先进技术的创新举措&#xff0c;煤矿监管电子封条算法通过在现场运料运人井口、回风井口、车辆出入口等关键位置进行人员进出、人数变化和设备开停等情况的识别和分析。YOLO检测速度非常快。标准版本的YOLO可以…

PY32F003F18P单片机概述

PY32F003F18P单片机是普冉的一款ARM微控制器&#xff0c;内核是Cortex-M0。这个单片机的特色&#xff0c;就是价格便宜&#xff0c;FLASH和SRAM远远超过8位单片机&#xff0c;市场竞争力很强大。 一、硬件资源&#xff1a; 1)、FLASH为64K字节&#xff1b; 2)、SRAM为8K字节&…

本地开机启动jar

1&#xff1a;首先有个可运行的jar包 本地以ruiyi代码为例打包 2&#xff1a;编写bat命令---命名为.bat即可 echo off java -jar D:\everyDay\test\RuoYi\target\RuoYi.jar 3&#xff1a;设置为开机自启动启动 快捷键winr----输入shell:startup---打开启动文档夹 把bat文件复…

NTP时钟同步服务器

目录 一、什么是NTP&#xff1f; 二、计算机时间分类 三、NTP如何工作&#xff1f; 四、NTP时钟同步方式&#xff08;linux&#xff09; 五、时间同步实现软件&#xff08;既是客户端软件也是服务端软件&#xff09; 六、chrony时钟同步软件介绍 七、/etc/chrony.conf配置文件介…

uniapp小程序单页面改变手机电量,头部通知的颜色效果demo(整理)

onShow(){ // 改变电池的颜色 wx.setNavigationBarColor({ frontColor: ‘#ffffff’, //只支持两种颜色 backgroundColor: ‘#ffffff’, animation: { duration: 1 } }) }

IP对讲终端SV-6005带一路2×15W或1*30W立体声做广播使用

IP对讲终端SV-6005双按键是一款采用了ARMDSP架构&#xff0c;接收网络音频流&#xff0c;实时解码播放&#xff1b;配置了麦克风输入和扬声器输出&#xff0c;SV-6005带两路寻呼按键&#xff0c;可实现对讲、广播等功能&#xff0c;作为网络数字广播的播放终端&#xff0c;主要…

【算法】leetcode 105 从前序与中序遍历序列构造二叉树

题目 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请构建该二叉树并返回其根节点。 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 示例 1: Input: preorder [3,9,20,15,7], inorder [9,3,15,20,7] Output: [3,9,20,null,null,15,7]示例 2: Input: pr…

[管理与领导-65]:IT基层管理者 - 辅助技能 - 4- 乌卡时代(VUCA )

前言&#xff1a; 大多数IT人&#xff0c;很勤奋&#xff0c;但都没有职业规划&#xff0c;被工作驱动着前行&#xff0c;然而&#xff0c;作为管理者&#xff0c;你就不能没有职业规划思维&#xff0c;因为你代表一个团队&#xff0c;你的思维决定了一个团队的思维。本文探讨…

springboot配置ym管理各种日记(log)

1&#xff1a;yml配置mybatis_plus默认日记框架 mybatis-plus:#这个作用是扫描xml文件生效可以和mapper接口文件使用&#xff0c;#如果不加这个,就无法使用xml里面的sql语句#启动类加了MapperScan是扫描指定包下mapper接口生效&#xff0c;如果不用MapperScan可以在每一个mapp…

Redis 缓存穿透、击穿、雪崩

一、缓存穿透 1、含义 缓存穿透是指查询一个缓存中和数据库中都不存在的数据&#xff0c;导致每次查询这条数据都会透过缓存&#xff0c;直接查库&#xff0c;最后返回空。 2、解决方案 1&#xff09;缓存空对象 就是当数据库中查不到数据的时候&#xff0c;我缓存一个空对象…