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;
}

三. 总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java-泛型

文章目录 Java泛型什么是泛型&#xff1f;在哪里使用泛型&#xff1f;设计出泛型的好处是什么&#xff1f;动手设计一个泛型泛型的限定符泛型擦除泛型的通配符 结论 Java泛型 什么是泛型&#xff1f; Java泛型是一种编程技术&#xff0c;它允许在编译期间指定使用的数据类型。…

操作视频的开始与暂停

调用 ref.current.play() 方法来播放视频&#xff1b; 如果视频需要暂停&#xff0c;我们调用 ref.current.pause() 方法来暂停视频。 通过 useRef 创建的 ref 操作视频的开始与暂停 当用户点击按钮时&#xff0c;根据当前视频的状态&#xff0c;我们会开始或暂停视频&…

ip地址、LINUX、与虚拟机

子网掩码&#xff0c;是用来固定网络号的&#xff0c;例如255&#xff0c;255,255,0&#xff0c;表明前面三段必须为网络号&#xff0c;后面必须是主机号&#xff0c;那么怎么实现网络复用呢&#xff0c;例如使用c类地址&#xff0c;但是正常子网掩码是255&#xff0c;255,255,…

GB28181学习(二)——注册与注销

概念 使用REGISTER方法进行注册和注销&#xff1b;注册和注销应进行认证&#xff0c;认证方式应支持数字摘要认证方式&#xff0c;高安全级别的宜支持数字证书认证&#xff1b;注册成后&#xff0c;SIP代理在注册过期时间到来之前&#xff0c;应向注册服务器进行刷新注册&…

vue从零开始学习

npm install慢解决方法:删掉nodel_modules。 5.0.3:表示安装指定的5.0.3版本 ~5.0.3:表示安装5.0X中最新的版本 ^5.0.3: 表示安装5.x.x中最新的版本。 yarn的优点: 1.速度快,可以并行安装 2.安装版本统一 项目搭建: 安装nodejs查看node版本:node -v安装vue clie : np…

C语言练习8(巩固提升)

C语言练习8 编程题 前言 奋斗是曲折的&#xff0c;“为有牺牲多壮志&#xff0c;敢教日月换新天”&#xff0c;要奋斗就会有牺牲&#xff0c;我们要始终发扬大无畏精神和无私奉献精神。奋斗者是精神最为富足的人&#xff0c;也是最懂得幸福、最享受幸福的人。正如马克思所讲&am…

明厨亮灶监控实施方案 opencv

明厨亮灶监控实施方案通过pythonopencv网络模型图像识别算法&#xff0c;一旦发现现场人员没有正确佩戴厨师帽或厨师服&#xff0c;及时发现明火离岗、不戴口罩、厨房抽烟、老鼠出没以及陌生人进入后厨等问题生成告警信息并进行提示。OpenCV是一个基于Apache2.0许可&#xff08…

01-虚拟机安装Windows Server操作系统

1、创建并配置虚拟机 2、安装操作系统 找到windows Server镜像 等待安装 3、设置密码

数据结构之单链表java实现

基本概念 链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中指针链接次序实现的。和数组相比较&#xff0c;链表不需要指定大小&#xff0c;也不需要连续的地址。 单链表的基本设计思维是&#xff0c;利用结构体的设置&#xff0c…

滑动窗口实例5(水果成篮)

题目&#xff1a; 你正在探访一家农场&#xff0c;农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示&#xff0c;其中 fruits[i] 是第 i 棵树上的水果 种类 。 你想要尽可能多地收集水果。然而&#xff0c;农场的主人设定了一些严格的规矩&#xff0c;你必须按…

垃圾回收 - 引用计数法

GC原本是一种“释放怎么都无法被引用的对象的机制”。那么人们自然而然就会想到&#xff0c;可以让所有对象事先记录下“有多少程序引用了自己”。让各对象知道自己的“人气指数”&#xff0c;从而让没有人气的对象自己消失&#xff0c;这就是引用计数法。 1、计数器 计数器表…

使用MATLAB解算炼油厂的选址

背景 记得有一年的数据建模大赛&#xff0c;试题是炼油厂的选址&#xff0c;最后我们采用MATLAB编写&#xff08;复制&#xff09;蒙特卡洛算法&#xff0c;还到了省级一等奖&#xff0c;这里把仅有一些记忆和材料&#xff0c;放到这里来&#xff0c;用来纪念消失的青春。 本…

selenium可以编写自动化测试脚本吗?

Selenium可以用于编写自动化测试脚本&#xff0c;它提供了许多工具和API&#xff0c;可以与浏览器交互&#xff0c;模拟用户操作&#xff0c;检查网页的各个方面。下面是一些步骤&#xff0c;可以帮助你编写Selenium自动化测试脚本。 1、安装Selenium库和浏览器驱动程序 首先…

Redis布隆过滤器原理

其实布隆过滤器本质上要解决的问题&#xff0c;就是防止很多没有意义的、恶意的请求穿透Redis&#xff08;因为Redis中没有数据&#xff09;直接打入到DB。它是Redis中的一个modules&#xff0c;其实可以理解为一个插件&#xff0c;用来拓展实现额外的功能。 可以简单理解布隆…

Educational Codeforces Round 154 (Rated for Div. 2)

Educational Codeforces Round 154 (Rated for Div. 2) A. Prime Deletion 思路&#xff1a; 因为1到9每个数字都有&#xff0c;所以随便判断也质素即可 代码 #include<bits/stdc.h> using namespace std; #define int long long #define rep(i,a,n) for(int ia;i<…

数学建模-点评笔记 9月3日

1.摘要&#xff1a;关键方法和结论&#xff08;精炼的语言&#xff09;要说明&#xff0c;方法的合理性和意义也可以说明。 评委先通过摘要筛选&#xff08;第一轮&#xff09; 2.时间序列找异常值除了3西格玛还有针对时间序列更合适寻找的方法 3.模型的优缺点要写的详细一点…

vue3 页面显示中文,分页显示中文

vue3 分页默认为英文 &#xff0c;但想要中文显示 那么在App.vue中的代码为三步即可&#xff0c;引入中文&#xff0c;声明中文 &#xff0c;绑定中文&#xff1b; 1. import zhCn from element-plus/es/locale/lang/zh-cn&#xff1b; 2. let locale zhCn; 3. :locale&q…

Snipaste_2023-08-22_16-09-41.jpg

原因 cd 目录名 (想要补全的时候出现以下错误) cd /o-bash: cannot create temp file for here-document: No space left on device解决方案 可以使用df -h命令查看磁盘空间的使用情况,删除一些不必要的文件或调整其他文件的存储位置来释放空间,或者还可以考虑扩大磁盘容量 df …

数据结构1 -- leetcode练习

三. 练习 3.1 时间复杂度 用函数 f ( n ) f(n) f(n) 表示算法效率与数据规模的关系&#xff0c;假设每次解决问题需要 1 微秒&#xff08; 1 0 − 6 10^{-6} 10−6 秒&#xff09;&#xff0c;进行估算&#xff1a; 如果 f ( n ) n 2 f(n) n^2 f(n)n2 那么 1 秒能解决多…

PHP NBA球迷俱乐部系统Dreamweaver开发mysql数据库web结构php编程计算机网页

一、源码特点 PHP NBA球迷俱乐部系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 基于PHP的NBA球迷俱乐部 二、功能介绍 1、前台主要功能&#xff1a; 系统首页 网站介…