文章目录
- 【Linux 下】 信号量
- 信号量概念
- 信号量操作
- 初始化和销毁
- P()操作
- V()操作
- 理解PV操作
- 基于信号量与环形队列实现的CS模型
- 基于信号量和环形队列实现的生产者与消费者模型
【Linux 下】 信号量
信号量概念
信号量(Semaphore)是一种用于实现线程或进程之间同步和互斥的机制。它是由一个计数器和一组相关操作组成。
信号量中的计数器可以表示可用的资源数量或某种状态信息。线程或进程可以通过对信号量进行操作来进行等待或释放资源。
信号量的操作主要有两种:
- P(Wait)操作:如果计数器大于0,则将计数器减1;如果计数器为0,则等待,直到计数器大于0才能继续执行。
- V(Signal)操作:将计数器加1,并唤醒等待的线程或进程。
简单理解信号量,就可以将信号量理解为一个计数器,记录了当前临界资源的数量多少
一个东西的出现,就必然有它出现的道理和用处,信号量可用于解决并发编程中的各种问题,例如资源的互斥访问、线程的同步等。通过合理地使用信号量,可以避免竞态条件、死锁等并发编程中常见的问题。
补充:
信号量可以是计数信号量(Counting Semaphore)或二进制信号量(Binary Semaphore)。
- 计数信号量:计数信号量可以取任意非负整数值,表示可用的资源数量。线程或进程可以通过 P 操作申请使用资源,通过 V 操作释放资源。当计数信号量的值为0时,表示资源已全部被占用,需要等待其他线程或进程释放资源。
- 二进制信号量:二进制信号量只能取0或1两个值。常用于实现互斥锁的功能。当二进制信号量的值为1时,表示资源可用,线程或进程可以继续执行。当值为0时,表示资源已被占用,线程或进程需要等待。
下面介绍的是计数信号量
信号量操作
初始化和销毁
sem_init
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GQmJgFH-1684512300347)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315112821896.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhSOCzL9-1684512300348)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315113337002.png)]
作用:初始化信号量
参数:
sem: 指向我们所需要初始化信号量的指针,即指针里面存放的是我们所需要初始化的信号量的地址
pshared: 决定该信号量存放的位置,和被线程间分享还是进程间分享
**value 😗*信号量的初始值
返回值:
成功返回0,失败返回-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-siYBSut0-1684512300348)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315113651137.png)]
sem_destroy
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bCFw7XFv-1684512300348)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315113751565.png)]
作用:销毁信号量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yf7zacnN-1684512300349)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315114108917.png)]
参数:
- sem: 所需要销毁的信号量的指针
返回值:
成功返回0,失败返回-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WRE4Nr7K-1684512300349)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315114133245.png)]
P()操作
sem_wait
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zUwWFTp4-1684512300349)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315114246512.png)]
作用:等待信号量,对信号量进行P()操作(–sem),本质是申请使用临界资源[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcAmmxan-1684512300349)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315114703457.png)]
参数:
- sem: 所要获取的信号量资源
返回值:
成功返回0,失败返回-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78ngFxTm-1684512300350)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315114931665.png)]
V()操作
sem_post
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KWtQwoVu-1684512300350)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315115042139.png)]
作用:对信号量进行V()操作(++sem),本质是释放临界资源[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Gw6AvFH-1684512300350)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315115155306.png)]
参数:
- sem: 所要获取的信号量资源
返回值:
成功返回0,失败返回-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VOC7HwJe-1684512300351)(C:\Users\LANSHUHANG\AppData\Roaming\Typora\typora-user-images\image-20230315115246400.png)]
理解PV操作
实际上p(),V()操作就是对信号量进行–,++操作,本质就是通过p()操作等待或者申请使用的临界资源,通过V()操作,将获得的临界资源归还或是释放;
从上面的理解我们不难得知,信号量也是临界资源,,而我们在前面的线程互斥曾说过,++,–并不是原子性操作,所以说sem的P(),V()操作实际上是极不安全的,这就说明对信号量的++,–操作是存在线程安全问题的;所以我们对信号量的操作是需要互斥锁的
基于信号量与环形队列实现的CS模型
补充概念
**并行(Parallel)和串行(Serial)**是描述多任务执行方式的概念。
串行指的是按顺序一个接一个地执行任务或指令。在串行执行中,每个任务或指令必须在前一个任务或指令完成之后才能开始执行。这意味着任务是按照线性顺序进行处理的,没有同时执行多个任务的能力。串行执行适用于那些互相依赖的任务,其中一个任务的结果需要作为输入传递给下一个任务。
并行是指同时执行多个任务或指令。在并行执行中,多个任务可以在同一时间段内同时进行,彼此之间相互独立。并行执行可以通过同时利用多个处理器核心、多线程或分布式计算等技术实现。并行执行可以大大提高任务的执行速度和系统的吞吐量,尤其适用于那些可以被分解成独立子任务的问题。
并行和串行之间存在一定的权衡和适用场景。串行执行简单直观,适用于顺序执行的任务,但可能导致执行时间较长。而并行执行可以提高任务的执行效率,但可能需要额外的并行处理能力和复杂的同步机制来确保正确性。
在现代计算机系统中,通常会同时应用并行和串行的概念。例如,一个程序可以使用串行方式执行某些任务,而使用并行方式执行某些可以并行处理的子任务。通过合理地组织和分配任务,可以充分利用系统资源,提高计算性能和效率。
并发(Concurrency)
**并发(Concurrency)**是指系统能够同时处理多个独立的任务、操作或事件的能力。并发的关键在于任务之间的重叠执行,不一定需要同时进行,但可以交替执行以提高效率和资源利用率。
并发可以在单个处理器上通过时间分片或任务切换实现,也可以利用多个处理器核心、多线程或分布式系统来实现。
并发编程涉及多个执行流(线程、进程或任务)同时执行,这些执行流可以独立运行并相互交互。并发编程的目标是保证多个执行流之间的正确同步和协调,避免竞态条件、死锁、饥饿等并发问题。
基于信号量和环形队列实现的生产者与消费者模型
合理的运用信号量可以实现线程并发操作
实现思路:
-
使用环形队列存储数据(逻辑上的数据:空位置和数据。配合指针来实现这个逻辑),充当“超市”,
-
控制环形队列的精髓
index %= _cap;
-
指针:p_step– 代表生产者当前所处的坐标,通常指向的是空位置资源处;c_step–代表消费者当前所处的坐标,通常指向的是数据资源处
-
-
通过互斥量和信号量维持生产者和消费者之间的关系
- 消费者关注的是环形队列中的产品资源,只要有数据资源,就进行消费,没有数据资源,就停下里等待数据资源
- 生产者关注的是环形对列中的空位置资源,只要有空位置,就进行“生产”,没有空位置,就停下等待空位置资源
- 使用俩个信号量描述空位置资源和数据资源的状态(empty_sem,data_sem)
-
规定环形队列大小为10,可以自行调节
-
同步实现精髓:
- p_step,c_step一开始都指向下标为0,代表生产者和消费同时出发,但得遵循以下规则
- 消费者永远都是在追随生产者的步伐–
- 生产者不能将消费者套圈–
- 初始empty_sem为10,data_sem为0;
- p_step,c_step一开始都指向下标为0,代表生产者和消费同时出发,但得遵循以下规则
一些说明:
- 因为生产者和消费者所关注的是不同的临界资源,所以使用俩把互斥锁,对不同的临界资源加锁,逻辑上更加合理,且效率更高
- 再理解:多生产者和多消费者模型的优势实际上在于可以并行处理的和生产数据
- 生产实际上分为俩步:1.生产数据 (通常是耗时的)2.分布数据(将数据添加到环形队列中)
- 消费也是分为俩步:1.取出数据 2.处理数据(耗时)
- 多个线程都能申请到临界资源(空位置资源或数据资源),而后就可以同时对数据处理;这是条件变量实现的cs模型所不能比拟的
主逻辑代码:
#include <iostream>
#include "Ringqueue.hpp"
#include <unistd.h>
#include <time.h>
using namespace Lsh_ring_queue;
void* Produce(void* args)
{
auto rq =(Ring_queue<int>* )args;
while(1)
{
//生产:1. 生产数据 2.将数据添加到环形队列中
int data=rand()%20+1;
std::cout<<"生产者->"<<pthread_self()<<"生产了: "<<data<<std::endl;
rq->Push(data);
sleep(1);
}
}
void* Comsumer(void * args)
{
auto rq=(Ring_queue<int>*) args;
while(1)
{
//消费: 1.从环形队列中取出数据 2.处理数据--此处只是进行了数据打印,实际中可以将该部分换成一些业务逻辑
int data;
rq->Pop(&data);
std::cout<<"消费者->"<<pthread_self()<<"消费了: "<<data<<std::endl;
sleep(1);
}
}
int main()
{
srand((unsigned int)time(nullptr));
pthread_t p;
pthread_t p1;
pthread_t p2;
pthread_t c;
pthread_t c1;
pthread_t c2;
Ring_queue<int> * rq=new Ring_queue<int>();
pthread_create(&p,nullptr,Produce,(void*)rq);
pthread_create(&p1,nullptr,Produce,(void*)rq);
pthread_create(&p2,nullptr,Produce,(void*)rq);
pthread_create(&c,nullptr,Comsumer,(void*)rq);
pthread_create(&c1,nullptr,Comsumer,(void*)rq);
pthread_create(&c2,nullptr,Comsumer,(void*)rq);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
return 0;
}
环形队列实现代码:
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
namespace Lsh_ring_queue
{
const int de_cap = 10;
template <class T>
class Ring_queue
{
private:
int _cap;
std::vector<T> _rq;
sem_t _blank_sem; // 生产者所关心的信号量 初始值为10 代表10个空格 初始值为10
sem_t _data_sem; // 消费者所关系的信号量 初始值为0 代表0个数据 初始值为0
int p_step; // 生产者和消费者所在的位置
int c_step;
pthread_mutex_t p_mtx;
pthread_mutex_t c_mtx;
public:
Ring_queue()
:_cap(de_cap) ,_rq(de_cap) ,p_step(0),c_step(0)
{
//第二个参数未0,代表在线程间共享该信号量
sem_init(&_blank_sem,0,10);
sem_init(&_data_sem,0,0);
pthread_mutex_init(&p_mtx,nullptr);
pthread_mutex_init(&c_mtx,nullptr);
}
void Push(T& in)
{
//对空位置进行减减操作
sem_wait(&_blank_sem);
pthread_mutex_lock(&p_mtx);
_rq[p_step]=in;
p_step++;
p_step%=_cap; //更新生产者的位置
pthread_mutex_unlock(&c_mtx);
sem_post(&_data_sem);
}
void Pop(T* out)
{
sem_wait(&_data_sem);
pthread_mutex_lock(&c_mtx);
*out=_rq[c_step];
c_step++;
c_step%=_cap;
pthread_mutex_unlock(&c_mtx);
sem_post(&_blank_sem);
}
~Ring_queue()
{
sem_destroy(&_blank_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&p_mtx);
pthread_mutex_destroy(&c_mtx);
}
};
}
运行结果:
pthread_mutex_unlock(&c_mtx);
sem_post(&_blank_sem);
}
~Ring_queue()
{
sem_destroy(&_blank_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&p_mtx);
pthread_mutex_destroy(&c_mtx);
}
};
}
**运行结果:**
![](https://img-blog.csdnimg.cn/img_convert/f99068201396b3ae7cb89b7577e41065.png)