Linux之信号量

前言

IPC中介绍过信号量, 为了让进程间通信, 从而多个执行流看到同一份公共资源, 对于并发访问造成数据不一致问题, 就需要把公共资源保护起来, 从而就需要同步与互斥.

信号量共有三个特性:

1. 本质是一把用于描述临界资源资源的数目的计数器

2每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源(对资源预定), 其实就是先对信号量计数器进行P操作. 本质上, 只要P成功. 完成了对资源的预订机制.

3. 信号量也是公共资源, 为了保证自身安全, PV操作是原子的.

为什么使用信号量 

我们实现基于阻塞队列的生产者消费者模型时,  为什么使用互斥锁而不是其它的管理同步互斥的资源? 

因为我们是把这个队列当作一个整体来用. 每个线程拿到该队列后, 对该队列的Push和pop操作是会互相影响的, 可能涉及空间的重新配置, 所以容器不是线程安全的.

所以阻塞队列的模型有以下缺点:

  • 访问临界资源前, 无法得知临界资源的情况, 所以要把临界资源当作一个整体使用
  • 多个线程不能同时访问临界资源的不同区域

为了解决上述问题, 现在我们想使用信号量管理同步与互斥. 如果当前存在一个数组公共资源:

1. 使用时并不把公共资源当作整体

2. 多线程不访问临界资源的同一个区域, 而是访问临界资源的不同区域

最好的情况下, 我们为公共资源里的 N 份分别分给 N 个线程去使用, 但是为了保证 N 个资源不分配给多于 N 个线程访问, 我们需要信号量的PV操作去管理资源:

  1. 申请信号量(P操作)
  2. 访问N个资源中指定的一个资源
  3. 释放信号量(V操作)

当信号量内计数器减到0的时候, 说明临界资源的 N 个区域都有线程访问. 其他想访问临界资源的线程由于申请信号量不成功, 就只能在信号量的阻塞队列中等待. 

其中与之前使用阻塞队列的最大不同之处在于: 我们通过信号量管理的资源在使用的时候不需要对其进行条件判断, 比如 while(isFull()); 因为之前线程拿到阻塞队列并不知道其中资源的使用情况(空或满), 需要判断; 而信号量管理的资源把判断结合到P操作中, 所以线程通过信号量预定的资源一定是可以使用的. 

举个例子, 我们去电影院线下买票时要问一问售票员是否还有票, 但是线上预定好票之后就不会再问了, 直接取票使用.

所以通过使用信号量:

  • 线程可以在不访问临界资源的情况下知晓资源的使用情况,   
  • 允许多线程同时访问共享资源的不同区域, 有效提升了程序的效率。

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

1. 认识信号量接口

首先, 信号量也是一个类(sem_t), 也可以构造对象(sem_t sem),对象内也有成员函数:

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

头文件:semaphore.h

功能:初始化信号量

参数:sem_t* sem 表示需要被初始化的信号量的地址. int pshared表示信号量共享模式, 0表示线程间共享, 非零表示进程间共享, 这里设为0. unsigned int value表示信号量的初始值, 也就是计数器的初始值.

返回值:初始化成功返回0,失败返回-1并将错误码设置进errno

int sem_destroy(sem_t* sem);

头文件:semaphore.h

功能:销毁信号量

参数:sem_t* sem表示需要被销毁的信号量的地址

返回值:初始化成功返回0, 失败返回-1并将错误码设置进errno

int sem_wait(sem_t* sem);

头文件:semaphore.h

功能:申请(等待)信号量, P操作

参数:sem_t* sem 表示需要申请的信号量地址

返回值:初始化成功返回0, 失败返回-1并将错误码设置进errno

int sem_post(sem_t* sem);

头文件:semaphore.h

功能:归还(发布)信号量, V操作

参数:sem_t* sem 表示需要归还的信号量地址。

返回值:初始化成功返回0, 失败返回-1并将错误码设置进errno

注意: 现在操作系统的 IPC 普遍使用 systemV 和 POSIX 两种标准. 正是因为信号量和互斥锁都使用了POSIX标准,所以信号量与互斥锁的接口非常类似

2. 环形队列 

数据结构中用数组模拟环形队列时, 通过 下标%总长度 得到逻辑上的环形队列.

基于Ringbuffer的PC模型的基本规则 

基于环形队列的生产者消费者模型需要遵守这几个规则:

1. 生产者不能把消费者套圈, 此时生产者生产完最后一个数据, 马上追上消费者, 如果生产者继续生产, 将覆盖原来生产的数据, 不可以.

2. 消费者不能超过生产者

这个也很容易理解, 消费者要等待生产者生产完数据才能消费.

图中消费者消费速度很快, 马上超过生产者, 所以此时就需要等待生产.

 

但是是否能在同一位置?

结论: 消费者和生产者在同一位置时, 要么为空, 要么为满. 在其它情况下, 两者根本不会指向同一个位置! 

a. 为空时, 只允许生产者生产, "只"体现了互斥, "生产者"体现了同步.

b. 为满时, 只运行消费者消费, "只"体现了互斥, "消费者"体现了同步.

数据结构中我们认为 front == rear 为空, (rear+1) % N == front 为满, 此处我们用信号量控制资源, 就不需要这种方式判空和满. 

清楚了模型的规则之后, 我们要对资源进行认识, 怎样可以通过信号量来控制空与满?

由于生产者只负责将数据生产到环形队列中, 当环形队列满了以后就不能生产了, 所以它只关心队尾后还有多少空间供它生产数据

由于消费者只负责从环形队列中取数据, 当环形队列空了以后就不能消费了, 所以它只关心队首到队尾有几个数据可以供它消费

所以我们得到:

生产者在意环形队列中空闲空间可存储数据的个数
消费者在意环形队列中数据的个数

于是我们就可以两个信号量来控制:

  • 生产者信号量 -> 管理空间
  • 消费者信号量 -> 管理数据

 生产和消费的伪代码大体上是这样的:

//生产者
P(空间)
生产者行为
V(资源)//消费者
P(资源)
消费者行为
V(空间)

多生产多消费模型 

 但是前面的理论叙述, 我们只体现出3个关系中"生产者与消费者的互斥与同步", 我们其实还没体现出生产者之间消费者之间的关系, 于是我们在上面的基础上对临界区加锁:

//生产者
P(空间)
加锁
生产者行为
释放锁
V(资源)//消费者
P(资源)
加锁
消费者行为
释放锁
V(空间)

有一个问题, 申请互斥锁信号量谁在前比较合适呢?

信号量在前, 申请锁在后

如果申请互斥锁在前, 申请信号量在后 , 会怎么样?

1. 假如现在我们只有一把锁:

如果一个生产者(或消费者)线程申请到锁后,信号量申请失败了,那线程就只能拿着锁阻塞,其他生产者(或消费者)线程就申请不到锁, 产生死锁, 整个程序就卡住了。

2. 假如现在我们有两把锁, 生产和消费各一把:

  • 由于有两把锁, 生产和消费互不影响, 程序不会产生死锁.
  • 但是对于线程来说, 申请锁是有代价的, 将信号量申请放在前面可以减少申请锁的次数

总结: 其实不管用什么形式控制临界资源去实现PC模型, 只要能满足 理论中的3个关系就可以.

代码实现 

 现在再来实现RingBuffer, 和之前一样, 用Task充当资源, 生产者线程和消费者线程的行为和基于阻塞队列的PC一样, 通过向队列Push和Pop数据来进行生产和消费:

#include "RingBuffer.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <iostream>
#include <string>std::string opers = "+-*/()";void *produce(void *arg)
{RingBuffer<Task> *prb = static_cast<RingBuffer<Task> *>(arg);while (true){int dataX = rand() % 10;usleep(100);int dataY= rand() % 10;usleep(100);char oper = opers[rand() % opers.size()];Task t(dataX, dataY, oper);prb->Push(t);std::cout << "Producer task: ";t.PrintTask();sleep(1);}}void *consumer(void *arg)
{RingBuffer<Task> *prb = static_cast<RingBuffer<Task> *>(arg);while (true){Task t;prb->Pop(&t);t.Run();usleep(100);std::cout << "Consumer task: ";t.PrintTaskResult();}
}int main()
{srand(time(nullptr));RingBuffer<Task> rb;pthread_t con[3], pro[2];for(auto& c: con)pthread_create(&c, nullptr, consumer, (void *)&rb);for(auto& p: pro)pthread_create(&p, nullptr, produce, (void *)&rb);for(auto& c: con)pthread_join(c, nullptr);for(auto& p: con)pthread_join(p, nullptr);return 0;
}

环形队列中需要有两个信号量控制生产者和消费者的互斥与同步, 两个锁控制生产者之间和消费者之间的互斥 .

此外, 为了插入和取出, 还需要两个下标来记录当前的位置.

#pragma once
#include <semaphore.h>
#include <pthread.h>
#include "LockGuard.hpp"
#include <vector>template <class T>
class RingBuffer
{
public:RingBuffer(int size = 10): _size(size), _q(_size), _c_pos(0), _p_pos(0){pthread_mutex_init(&_p_mutex, nullptr);pthread_mutex_init(&_c_mutex, nullptr);sem_init(&_p_sem, 0, _size);sem_init(&_c_sem, 0, 0);}void P(sem_t *sem){sem_wait(sem);}void V(sem_t *sem){sem_post(sem);}void Push(const T &in){P(&_p_sem);{LockGuard lg(&_p_mutex);_q[_p_pos++] = in; // 放入数据_p_pos %= _size;   // 更新生产者位置}V(&_c_sem);}void Pop(T *out){P(&_c_sem);{LockGuard lg(&_c_mutex);*out = _q[_c_pos++]; // 取出数据_c_pos %= _size;   // 更新消费者位置}V(&_p_sem);}private:size_t _size;//队列大小std::vector<T> _q;// 环形队列sem_t _p_sem;//生产者信号量, 管理空间sem_t _c_sem;// 消费者信号量, 管理数据pthread_mutex_t _c_mutex;//消费者互斥pthread_mutex_t _p_mutex;//生产者互斥int _c_pos;//消费者当前位置int _p_pos;///生产者当前位置
};

运行程序, 我们有3个消费者和2个生产者. 由于我们假设消费者消费速度很快, 生产者1s生产一个数据, 所以结果呈现出每秒有两个消费者竞争去消费生产者生产的数据.

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

eval长度限制绕过

我把他的叙述写成代码&#xff0c;大概如下&#xff1a; <?php $param $_REQUEST[param]; if(strlen($param)<17 && stripos($param,eval) false && stripos($param,assert) false) {eval($param); } ?> 那么这个代码怎么拿到webshell&#xf…

Linux - 进程间通信(管道)

文章目录 一、进程间通信的目的二、进程间通信的本质三、管道1、介绍2、匿名管道3、命名管道 一、进程间通信的目的 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程资源共享&#xff1a;多个进程之间共享同样的资源。通知事件&#xff1a;一个进程需要向另一个或…

【软考】反规范化技术

论反规范化技术 反规范化有这几种技术&#xff0c;增加冗余列&#xff0c;增加派生列&#xff0c;重组表和分割表。其中冗余列是指同一个字段在另外的表中存储一份&#xff0c;减少连表操作。增加派生列是基于另外一个列或者多个列&#xff0c;计算得到一个新的列&#xff0c;可…

SpringBoot day 1104

ok了家人们这周学习SpringBoot的使用&#xff0c;和深入了解&#xff0c;letgo 一.SpringBoot简介 1.1 设计初衷 目前我们开发的过程当中&#xff0c;一般采用一个单体应用的开发采用 SSM 等框架进行开发&#xff0c;并在 开发的过程当中使用了大量的 xml 等配置文件&#x…

Python | Leetcode Python题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; class Solution:def __init__(self, w: List[int]):self.pre list(accumulate(w))self.total sum(w)def pickIndex(self) -> int:x random.randint(1, self.total)return bisect_left(self.pre, x)

C++ | Leetcode C++题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; class Solution { private:mt19937 gen;uniform_int_distribution<int> dis;vector<int> pre;public:Solution(vector<int>& w): gen(random_device{}()), dis(1, accumulate(w.begin(), w.end(), 0)) {partial_sum(…

弹簧质点系统求Hessian

Verification https://www.matrixcalculus.org/ (1-l0/norm2(p-q))*(p-q)

游游的游戏大礼包

游游的游戏大礼包 import java.util.*; public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);long n in.nextInt();long m in.nextInt();long a in.nextInt();long b in.nextInt();long ret 0;for(long x 0; x < Math.…

详解ARM汇编条件标志

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 条件标志 在 ARM 指令集中&#xff0c;条件标志是控制指令执行的一种机制&#xff0c;它们用于实现条件分支、比较和其他逻辑操作。 我们平时使用 IDA 调试程…

Navicat Premium安装卸载及使用教程教程

Navicat Premium 17 安装卸载及使用教程教程 0. 卸载 没安装过 Navicat 直接跳过本步骤即可。 正常卸载顺序即可&#xff0c;网上很多教程&#xff0c;这里不演示了 如果怕卸载不干净&#xff0c;最后时候可以执行一下压缩包里面的无限试用 Navicat.bat 即可成功删除Navicat…

Backbone网络详解

Backbone 网络&#xff08;主干网络&#xff09;是深度学习模型中的一个重要组成部分&#xff0c;尤其在计算机视觉任务中。Backbone 网络的主要作用是从输入数据中提取有用的特征&#xff0c;为后续的任务&#xff08;如分类、检测、分割等&#xff09;提供强大的特征表示。常…

Jenkins找不到maven构建项目

有的可能没有出现maven这个选项 解决办法&#xff1a;需要安装Maven项目插件 输入​Maven Integration plugin​

【339】基于springboot的新能源充电系统

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;新能源充电系统的设计与实现 摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解…

Godot Zelda教程练习1

提示&#xff1a;B站链接&#xff1a;Godot Zelda教程练习 资产链接&#xff1a;项目资产 Godot版本&#xff1a;4.3 文章目录 一、新建项目1、创建项目2、设置项目标签3、项目基本设置4、导入资产 二、图块集和自动平铺1、创建TileMapLayer2、创建Terrian Set1、Match Siders(…

【案例】旗帜飘动

开发平台&#xff1a;Unity 6.0 开发工具&#xff1a;Shader Graph 参考视频&#xff1a;Unity Shader Graph 旗帜飘动特效   一、效果图 二、Shader Graph 路线图 三、案例分析 核心思路&#xff1a;顶点偏移计算 与 顶点偏移忽略 3.1 纹理偏移 视觉上让旗帜保持动态飘动&a…

Android亮屏Job的功耗优化方案

摘要: Job运行时会带来持锁的现象,目前灭屏放电Job的锁托管已经有doze和绿盟标准监管,但是亮屏时仍旧存在过长的持锁现象,故为了优化功耗和不影响用户体验下,新增亮屏放电下如果满足冻结和已运行过一次Job,则进行job限制,当非冻结时恢复的策略 1.现象: (gms_schedu…

Java面试经典 150 题.P55. 跳跃游戏(009)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解&#xff1a; class Solution {public boolean canJump(int[] nums) {int…

源码解析篇 | YOLO11:计算机视觉领域的新突破 !对比YOLOv8如何 ?

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。在2024年9月27日盛大举行的YOLO Vision 2024活动上&#xff0c;Ultralytics公司震撼发布了YOLO系列的最新成员—YOLO11。作为Ultralytics YOLO系列实时目标检测器的最新迭代&#xff0c;YOLO11凭借尖端的准确性、速度和效率…

mac m1 docker本地部署canal 监听mysql的binglog日志

mac m1 docker本地部署canal监听mysql的binglog日志(虚拟机同理) 根据黑马视频部署 1.docker 部署mysql 1.docker拉取mysql 镜像 因为m1是arm架构.需要多加一条信息 正常拉取 docker pull mysql:tagm1拉取 5.7的版本. tag需要自己指定版本 docker pull --platform linux/x…

复现LLM:带你从零训练tokenizer

1. 引言 分词器是每个大语言模型必不可少的组件&#xff0c;但每个大语言模型的分词器几乎都不相同。如果要训练自己的分词器&#xff0c;可以使用huggingface的tokenizers框架&#xff0c;tokenizers包含以下主要组件&#xff1a; Tokenizer: 分词器的核心组件&#xff0c;定…