前言
本篇文章我们来探讨信号量的概念,我会先从信号量是什么来介绍信号量,然后从为什么会产生信号量这个概念来带大家一起理解信号量,最后我会通过一个基于信号量的环形队列的生产者消费者模型带大家来学习信号量是如何使用的、
如果觉得本篇文章写的不错的话,希望留下宝贵的点赞(这个对我非常非常非常重要)、关注加收藏,您的支持是我创作的最大动力!!!!
信号量
概念理解
什么是信号量呢?前面我们生产者消费者模型里面,我们是把资源整体进行使用,然后通过信号量去控制,每一次只让一个线程进入临界区进行访问,解决线程饥饿的问题;除了这个方法,我们还有没有其他的解决方法呢?
如果我们把一个共享资源分成许多份,每一个线程都去访问其中的一份,这样我们不就可以让多线程并发访问一个公共的资源了
如果我们对共享资源进行整体使用,那么就需要有互斥锁,为了防止线程饥饿,所以就需要有信号量;如果我们对共享资源进行局部使用,我们就需要控制进入共享资源的线程数量
那么,对于局部访问的话,我们就需要解决以下几个问题:
- 进入共享资源的线程数量超过资源块的数量
- 不同的线程访问了同一个位置
为了解决上面的问题,我们引入了信号量,其本质是一个计数器,为了描述临界资源的数量,未来每一个线程进入临界资源时,申请资源,计数器需要–,我们称之为P操作;线程出临界资源时,释放资源,计数器需要++,我们称之为V操作;当计数器小于等于0时,没有资源,就不允许线程进入。所以申请信号量,本质是对共享资源的预定机制
但是我们会发现,线程申请和释放信号量前提是需要看到同一份信号量,为了让线程看到同一份信号量,那么这个信号量本身就是一份共享资源,那么我们如何保证信号量的安全呢?
答案是:我们需要保证申请/释放信号量的PV操作时原子的
信号量接口
创建/初始化信号量

- 信号量类型:sem_t
- pshard:表明该信号量是进程间使用还是线程间使用,传入0表示线程间使用
- int value:信号量计数器初始值
- 返回值:成功返回0,失败返回-1
信号量销毁

申请信号量/P操作

释放信号量/V操作

使用方法
那么对于多线程之间,我们想使用信号量,我们就需要定义一个全局的信号量,线程进入共享资源的时候进行P操作,申请信号量,信号量–;离开共享资源的时候进行V操作,释放信号量,信号量++
那么对于多进程之间,我们可以使用共享内存,使用一个指针指向共享内存开头的位置,将指针强转成信号量类型,初始化信号量,使用完毕销毁信号量
基于信号量的环形队列的生产者消费模型
我们可以把环形队列抽象成线性的一维数组,定义两个指针,分别指向数组的头和尾,当尾部指针走到数组的尾部的时候,重新从开头继续向后,也就是说,我们可以使用数组模拟一个环形队列,从而在逻辑上变成一个环形队列,当首尾指针相遇的时候,一定为空或为满,如果不相等,那么一定访问的不是同一个位置;为了判断环形队列是空还是满,我们可以让数组的最后一个元素始终为空,然后去判断尾指针的下一个位置是否为头,如果指向头,说明此时环形队列已经满了,那么我们就不再插入元素
那么我们该如何去实现一个基于信号量的环形队列的生产者消费模型呢?
我们可以把头部指针作为一个消费者,把尾部指针作为一个生产者,为空的时候,必须让生产者先运行,此时我们就保证了生产与消费的互斥与同步关系;为满的时候,生产和消费又指向同一个位置了,必须让消费者先运行,此时我们又保证了生产与消费的互斥与同步关系;不为空和不为满时,我们就可以保证生产和消费不属于同一个位置,此时就可以并发运行
综上所述:
- 为空的时候,必须让生产者先运行,因为消费者不能超过生产者
- 为满的时候,必须让消费者先运行,因为生产者不能套圈消费者
- 不为空不为满,并发运行
那么在这个模型里面,生产者最关心的是空余空间的数量;消费者最关心的是数据资源的数量;生产者进行P操作就是申请空间资源,V操作就是归还数据资源;消费者进行P操作就是申请数据资源,V操作就是归还空间资源
那么我们根据上面的思路,就可以写下如下代码:
RingQueue.hpp
#pragma once
#include<iostream>
#include<vector>
#include"Sem.hpp"
const static int gcap=5;
template<typename T>
class RingQueue
{
public:
RingQueue(int cap=gcap)
:_cap(cap),
_ringqueue(cap),
_space_sem(cap),_data_sem(0),
_p_step(0),_c_step(0)
{}
void Pop(T*out)
{
_data_sem.P();
*out=_ringqueue[_c_step++];
_c_step%=_cap;
_space_sem.V();
}
void EnQueue(const T&in)
{
_space_sem.P();
//生产数据
_ringqueue[_p_step++]=in;
//判断是否越界
_p_step%=_cap;
_data_sem.V();
}
~RingQueue(){}
private:
std::vector<T> _ringqueue; //临界资源
int _cap;
Sem _space_sem;
Sem _data_sem;
//生产和消费的位置
int _p_step;
int _c_step;
};
Sem.hpp
#pragma once
#include<iostream>
#include<semaphore.h>
class Sem
{
public:
Sem(int num):_num(num)
{
sem_init(&_sem,0,_num);
}
void P()
{
int n=sem_wait(&_sem);
(void)n;
}
void V()
{
int n = sem_post(&_sem);
(void)n;
}
~Sem()
{
sem_destroy(&_sem);
}
private:
sem_t _sem;
int _num;
};
main.cc
#include"RingQueue.hpp"
#include<pthread.h>
#include<unistd.h>
void*consumer(void*args)
{
RingQueue<int>*rq=static_cast<RingQueue<int>*>(args);
while(true)
{
sleep(1);
int data=0;
rq->Pop(&data);
std::cout<<"消费了一个数据:"<<data<<std::endl;
}
}
void*productor(void*args)
{
RingQueue<int>*rq=static_cast<RingQueue<int>*>(args);
int data=1;
while(true)
{
rq->EnQueue(data);
std::cout<<"生产了一个数据:"<<data<<std::endl;
data++;
}
}
int main()
{
RingQueue<int>*rq=new RingQueue<int>();
pthread_t c,p;
pthread_create(&c,nullptr,consumer,(void*)rq);
pthread_create(&p,nullptr,productor,(void*)rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
问题分析
-
我们看上面的代码,对于信号量的环形队列而言,我们并没有加锁,那么能否保证数据的安全呢?
我们通过信号量,变相的完成了为空和为满的同步和互斥动作
-
我们发现,在PV操作之间的临界区内部,我们没有判断资源是否就绪,就直接进行操作,这样做正确吗?
答案在于我们申请信号量实际上是对资源的预定机制,所以在进行P操作时,本质就是对资源是否就绪做判断,我们将判断从临界区内部转移到了临界区外部或入口处
-
我们访问资源可以使用整体访问和局部访问,如果我们把信号量初始值设置为1,那么我们该怎么区理解呢?
实际上,二元信号量就是锁!!!锁的本质是互斥的申请资源,申请锁不就是P操作,释放锁不就是V操作吗,最终我们可以知道,锁就是信号量的一种特殊情况
-
那么我们变成多生产多消费,我们应该怎么做?
答案是,我们如果想要变成多生产多消费,那么我们就必须对生产者之间进行加锁,对消费者之间进行加锁,维护同步与互斥关系
网硕互联帮助中心


评论前必须登录!
注册