Liunx系统编程:信号量
一. 信号量概述
1.1 信号量的概念
在多线程场景下,我们经常会提到临界区和临界资源的概念,如果临界区资源同时有多个执行流进入,那么在多线程下就容易引发线程安全问题。
为了保证线程安全,互斥被引入,互斥可以保证在同一时刻只有一个执行流进入临界区访问临界资源,由于整个临界区都只允许一个执行流进入,我们可以认为互斥是将临界区当做一个整体来使用的。
但是,如图1.1,假设下面这种场景,一个临界区资源被分为N个小区域,每个小区域都有特定的数据,如果多个执行流同时访问同一个小区域,那么线程之间就会相互干扰,存在线程不安全问题,但如果多个执行流在某一时刻访问不同的小区域,保证每个小区域在同一时刻不会有多个执行流访问,那么即使有多个执行流进入临界区,也不存在线程安全问题。
结论:多个同时进入临界区的执行流,如果不访问同一块资源,就不会有线程不安全问题。

为了让多跟线程能同时访问临界资源,并且保证线程安全,信号量的概念被引入,以保证进入临界区的线程不访问同一块临界资源,以此来提高多线程的效率。
信号量的本质为计数器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为环形队列的逻辑结构和物理结构图,在其底层实现代码中,依旧是采用线性数组来实现的,只不过我们通过特定的计算机代码,来使其行为与首尾相连的环形结构一致。

假设环形队列能够容纳N个元素,那么我们在拿到下标为index的位置时,如要找到其后面第k个元素的位置,计算方法为:(index + k) % N。
有两种方法,可以判断环形队列是空还是满:
- 用计数器来辅助:如果计数器count = 0,环形队列就是空,如果等同于环形队列的最大容量N,即count = N,就是满。
- 间隔空位:相比于环形队列的最大容量,多开辟一个数据空间,采用两个指针first和last记录首个元素位置和末尾元素后面的位置,如果last == fisrt,那么环形队列为空,如果(last + 1) % N == first 成立,那么环形队列为满,图2.2为这种方法的。

2.2 生产消费者模型与环形队列的联系
如果采用阻塞队列的方式来实现生产与消费者模型,由于C++ STL中提供的queue不向用户暴露底层实现,并且将阻塞队列视为一个整体来进行数据的写入和读取,造成了某一时刻只允许一个生产者线程或一个消费者线程访问临界资源(阻塞队列),为了保证线程安全,生产者写数据和消费者读数据不能够同时进行。
如图2.3所示,假设我们希望向p_step所指向的位置写数据,从c_step所指向的位置读数据,由于p_step和c_step所指向的是环形队列的不同位置,此时生产者和消费者线程如果并发执行,不会出现线程不安全问题,因为这两个执行流访问的是临界资源的不同区域。
但是,如果p_step和c_step指向环形队列的同一位置,此时生产者线程和消费者线程并发执行,则会访问临界资源的相同区域,引发线程不安全问题。
允许一定条件下的生产者线程和消费者线程并发执行,可以显著降低等待时间,提高程序整体的运行效率。

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;
}
三. 总结
- 信号量的本质为一计数器,用于表示临界区内还剩多少资源。
- 通过使用信号量,可让多个线程执行流去访问临界资源的不同区域,达到某时刻多个执行流进入临界区,但不会造成线程不安全的目的。
- 线程进入临界区前要先申请信号量,在离开临界区后要释放信号量。
相关文章:

Liunx系统编程:信号量
一. 信号量概述 1.1 信号量的概念 在多线程场景下,我们经常会提到临界区和临界资源的概念,如果临界区资源同时有多个执行流进入,那么在多线程下就容易引发线程安全问题。 为了保证线程安全,互斥被引入,互斥可以保证…...

大集合按照指定长度进行分割成多个小集合,用于批量多次处理数据
📚目录 拆分案例拆分的核心代码 通常我们对集合的更新或者保存都需要用集合来承载通过插入的效率,但是这个会遇到一个问题就是你不知道那天那个集合的数量可能就超了,虽然我们连接数据库进行批量提交会在配置上配置allowMultiQueriestrue,但是…...

ELK日志收集系统集群实验(5.5.0版)
目录 前言 一、概述 二、组件介绍 1、elasticsearch 2、logstash 3、kibana 三、架构类型 四、ELK日志收集集群实验 1、实验拓扑 2、在node1和node2节点安装elasticsearch 3、启动elasticsearch服务 4、在node1安装elasticsearch-head插件 5、测试输入 6、node1服…...

基于java swing和mysql实现的电影票购票管理系统(源码+数据库+运行指导视频)
一、项目简介 本项目是一套基于java swing和mysql实现的电影票购票管理系统,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含:项目源码、项目文档、数据库脚本等,该项目附带全部源码可作为毕设使用。 项目都…...

数据结构--6.0最短路径
目录 一、迪杰斯特拉算法(Dijkstra) 二、弗洛伊德算法(Floyd) 在网图和非网图中,最短路径的含义是不同的。 ——网图是两顶点经过的边上的权值之和最少的路径。 …...

Docker进阶:mysql 主从复制、redis集群3主3从【扩缩容案例】
Docker进阶:mysql 主从复制、redis集群3主3从【扩缩容案例】 一、Docker常规软件安装1.1 docker 安装 tomcat(默认最新版)1.2 docker 指定安装 tomcat8.01.3 docker 安装 mysql 5.7(数据卷配置)1.4 演示--删除mysql容器…...

遗传算法决策变量降维的matlab实现
1.案例背景 1.1遗传算法概述 遗传算法是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。它最初由美国Michigan大学的J. Holland教授提出,1967年, Holland 教授的学生 Bagley在其博士论文中首次提出了“遗传…...

基于Open3D和PyTorch3D读取三维数据格式OBJ
本节将讨论另一种广泛使用的3D数据文件格式,即OBJ文件格式。OBJ文件格式最初由Wavefront Technologies Inc.开发。与PLY文件格式类似,OBJ格式也有ASCII版本和二进制版本。二进制版本是专有的且未记录文档。本章主要讨论ASCII版本。 与之前类似,将通过示例来学习文件格式。第…...

带纽扣电池产品出口澳洲安全标准,纽扣电池IEC 60086认证
澳大利亚政府公布了《消费品(纽扣/硬币电池)安全标准》和《消费品(纽扣/硬币电池)信息标准》。届时出口纽扣/硬币电池以及含有纽扣/硬币电池产品到澳大利亚的供应商,必须遵守这些标准中的要求。 一、 安全标准及信息标…...

spring高级源码50讲-37-42(springBoot)
Boot 37) Boot 骨架项目 如果是 linux 环境,用以下命令即可获取 spring boot 的骨架 pom.xml curl -G https://start.spring.io/pom.xml -d dependenciesweb,mysql,mybatis -o pom.xml也可以使用 Postman 等工具实现 若想获取更多用法,请参考 curl …...

腾讯云、阿里云、华为云便宜云服务器活动整理汇总
云服务器的选择是一个很重要的事情,避免产生不必要的麻烦,建议选择互联网大厂提供的云计算服务,腾讯云、阿里云、华为云就是一个很不错的选择,云服务器稳定性、安全性以及售后各方面都更受用户认可,下面小编给大家整理…...

L1-055 谁是赢家(Python实现) 测试点全过
前言: {\color{Blue}前言:} 前言: 本系列题使用的是,“PTA中的团体程序设计天梯赛——练习集”的题库,难度有L1、L2、L3三个等级,分别对应团体程序设计天梯赛的三个难度。更新取决于题目的难度,…...

开发一个npm包
1 注册一个npm账号 npm https://www.npmjs.com/ 2 初始化一个npm 项目 npm init -y3编写一段代码 function fn(){return 12 }exports.hellofn;4发布到全局node_module npm install . -g5测试代码 创建一个text文件 npm link heath_apisnode index.js6登录(我默认的 https…...

介绍几种使用工具
FileWatch,观测文件变化,源码地址:https://github.com/ThomasMonkman/filewatch nlohmann::json,json封装解析,源码地址:https://github.com/nlohmann/json optionparser,解析选项,源…...

Vue:关于声明式导航中的 跳转、高亮、以及两个类名的定制
声明式导航-导航链接 文章目录 声明式导航-导航链接router-link的两大特点(能跳转、能高亮)声明式导航-两个类名定制两个高亮类名 实现导航高亮,实现方式其实,css,JavaScript , Vue ,都可以实现。其实关于路由导航&…...

Sharding-JDBC分库分表-自动配置与分片规则加载原理-3
Sharding JDBC自动配置的原理 与所有starter一样,shardingsphere-jdbc-core-spring-boot-starter也是通过SPI自动配置的原理实现分库分表配置加载,spring.factories文件中的自动配置类shardingsphere-jdbc-core-spring-boot-starter功不可没,…...

E8267D 是德科技矢量信号发生器
描述 最先进的微波信号发生器 安捷伦E8267D PSG矢量信号发生器是业界首款集成式微波矢量信号发生器,I/Q调制最高可达44 GHz,典型输出功率为23 dBm,最高可达20 GHz,对于10 GHz信号,10 kHz偏移时的相位噪声为-120 dBc/…...

Git git fetch 和 git pull 区别
git pull和git fetch的作用都是用于从远程仓库获取最新代码,但它们之间有一些区别。 git pull会自动执行两个操作:git fetch和git merge。它从远程仓库获取最新代码,并将其合并到当前分支中。 示例:运行git pull origin master会从…...

软件UI工程师工作的岗位职责(合集)
软件UI工程师工作的岗位职责1 职责: 1.负责产品的UI视觉设计(手机软件界面 网站界面 图标设计产品广告及 企业文化的创意设计等); 2.负责公司各种客户端软件客户端的UE/UI界面及相关图标制作; 3.设定产品界面的整体视觉风格; 4.参与产品规划构思和创意过程&…...

Mac系统Anaconda环境配置Python的json库
本文介绍在Mac电脑的Anaconda环境中,配置Python语言中,用以编码、解码、处理JSON数据的json库的方法;在Windows电脑中配置json库的方法也是类似的,大家可以一并参考。 JSON(JavaScript Object Notation)是一…...

Python数据分析与数据挖掘:解析数据的力量
引言: 随着大数据时代的到来,数据分析和数据挖掘已经成为许多行业中不可或缺的一部分。在这个信息爆炸的时代,如何从大量的数据中提取有价值的信息,成为了企业和个人追求的目标。而Python作为一种强大的编程语言,提供…...

我的私人笔记(安装hive)
1.hive下载:Index of /dist/hive/hive-1.2.1 或者上传安装包至/opt/software:rz或winscp上传 2.解压 cd /opt/software tar -xzvf apache-hive-1.2.1-bin.tar.gz -C /opt/servers/ 3.重命名 mv apache-hive-1.2.1-bin hive 4.配置环境变量 vi /etc/…...

【kubernetes】k8s部署APISIX及在KubeSphere使用APISIX
Apache APISIX https://apisix.apache.org/ 功能比nginx-ingress更强 本文采用2.5.0版本 https://apisix.apache.org/zh/docs/apisix/2.15/getting-started/ 概述内容来源于官方,学习于马士兵云原生课程 概述 Apache APISIX 是什么? Apache APISIX 是 …...

串口接收数据-控制LED灯
目标 通过串口接收数据,对数据分析,控制8个LED灯按照设定时间闪烁。 8个LED灯可以任意设计,是否闪烁。闪烁时间按ms计算,通过串口发送,可设置1~4,294,967,296ms,也就是4字节数据协议自拟,有数…...

python面试题合集(一)
python技术面试题 1、Python中的幂运算 在python中幂运算是由两个 **星号运算的,实例如下: >>> a 2 ** 2 >>> a 4我们可以看到2的平方输出结果为4。 那么 ^指的是什么呢?我们用代码进行演示: >>>…...

论文浅尝 | 利用对抗攻击策略缓解预训练语言模型中的命名实体情感偏差问题...
笔记整理:田家琛,天津大学博士,研究方向为文本分类 链接:https://ojs.aaai.org/index.php/AAAI/article/view/26599 动机 近年来,随着预训练语言模型(PLMs)在情感分类领域的广泛应用,…...

springboot web开发springmvc自动配置原理
前言 我们也知道springboot启用springmvc基本不用做什么配置可以很方便就使用了但是不了解原理,开发过程中遇到点问题估计就比较头疼,不管了解的深不深入,先巴拉一番再说… 下面我们先看看官网…我的版本是2.3.2版本,发现官网改动也比较大…不同版本自己巴拉下吧,结构虽然变化…...

发表于《自然》杂志:语音转文本BCI的新突破实现62字/分钟的速度
语音脑机接口(BCI)是一项创新技术,通过用户的大脑信号在用户和某些设备之间建立通信通道,它们在恢复残疾患者的言语和通信能力方面具有巨大潜力。 早期的研究虽然很有希望,但尚未达到足够高的精度来解码大脑活动&…...

微软 Turing Bletchley v3视觉语言模型更新:必应搜索图片更精准
据微软新闻稿透露,在推出第三代Turing Bletchley视觉语言模型后,微软计划逐步将其整合到Bing等相关产品中,以提供更出色的图像搜索体验。这款模型最初于2021年11月面世,并在2022年秋季开始邀请用户测试。 凭借用户的反馈和建议&am…...

Ubuntu 22.04 x86_64 源码编译 pytorch-v2.0.1 笔记【2】编译成功
20230831继续: 当前状态 (pytorch-build) yeqiangyeqiang-MS-7B23:~/Downloads/src/pytorch$ pwd /home/yeqiang/Downloads/src/pytorch (pytorch-build) yeqiangyeqiang-MS-7B23:~/Downloads/src/pytorch$ python3 -V Python 3.10.6 (pytorch-build) yeqiangyeqi…...