💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、信号量的概念
- 二、POSIX信号量
- 三、总结
前言
今天我们来讲解一下信号量,相比较之前学习的多线程中的互斥锁来说,信号量的概念比互斥锁要难理解,但是博主会使用生活中的例子,来给大家讲解,最后会得出互斥锁其实是信号量的一种特殊情况,但不是信号量,而理解的本质却是一样的,最后博主会使用代码给大家演示信号量是怎么实现多线程的同步互斥的
一、信号量的概念
为什么会有信号量,他也是解决我们多线程访问共享资源的安全问题,之前的锁已经帮助我们解决了这个问题,那为什么还需要信号量呢??计算机中任何一种东西的产生都有他适用的场景,所以信号量的产生是为了适用于其他场景的。我们的锁他的作用是将一整块共享资源都保护起来,让一个执行流去访问,万一这个执行流只访问这块共享资源的一部分,那么就没有必要把整块都保护起来,只需要保护他要访问共享资源的一部分就可以了,所以我们就可以将这块共享资源分成许多份,而信号量就是表示分成多少份的数量,信号量(信号灯)的本质就是一把计算器,类似于int cnt=n(不是等于),用于描述临界资源数量的多少。
讲一个故事:
作为大学生,常见的娱乐就是看电影,电影院会在电影开始前,会卖票(票就是我们所以人的共享资源),假设电影院有100个座位,那么商家就会放出100张不重复的票(信号量就是100,表示临界资源有100个),他不会放多,也不会放少。此时我们就可以得出下面三个特点:
- 当我们看电影的时候,我们还没有去电影院,先买票–买票的本质就是对资源的预定机制(就是申请信号量的过程)
- 当我们买了一张票的时候,计数器就会减1,资源就是少1
- 当计数器到0之后,资源已经被申请完毕了
相比较之前的锁是对整个临界资源进行保护,而信号量则是对整体里面的一个小块进行预订,当多个执行流过来访问这个临界资源,必须先申请信号量,申请成功了这块资源就是自己的了,所以此时我们最怕的是:1.多个执行流访问一个小块的资源。2.n个资源,但有N+条执行流的时候必然会造成前面一点,所以信号量的作用机来了,信号量的数目就是资源数量,而执行流想要访问资源就必须申请信号量成功,此时信号量就会减1,当你使用资源的时候,就要释放信号量,此时信号量加1,这也是信号量的工作机制,也可以很好的解决第二点,对于第一点,如果有多个执行流来访问同一个资源,就可以把这个小块当成整体,在这个小块的临界资源上加锁,来实现互斥,对于这一整块临界资源,不就可以用一个剩余票数信号量表示,也可以设置一个卖出票数信号量表示,这样两个信号量自己申请信号量,释放对方的信号量,这个一会在案例当中会非常明显。 通过上面描述得出下面四点结论:
- 申请计数器成功,就表示我具有访问资源的权限了,就好比买到票了,座位就是我的,不管我去不去。
- 申请了计数器资源,我当前访问我要的资源了吗??没有。申请了计数器资源就是对资源的余地给机制。
- 计数器可以有效保证进入共享资源的执行流数量
- 所以每一个执行流,想要访问共享资源的一部分的时候,不是直接访问,而是先申请计数器资源
在此理解:
如果我们电影院里面只有一个座位呢??说明我们的临界资源数目就不是之前的100,而是1,所以信号量的数目就是1,只有一个人能抢到票,只有一个人可以看电影,也就是看电影期间只有一个执行流在访问临界资源,这个在之前说过的,当临界资源只能有一个执行流去访问这不就是之前说的线程互斥嘛。我们把信号量的值只能为1,0两态的计数器叫做二元信号量----本质就是锁
所以博主一开始说什么锁其实是信号量的一个特殊情况,但不是一个动心,本质理解是一样的。
其实让资源为1,这是程序员规定的,假设资源有100份,你也不可能设置成90份,也不可能设置多,设置为1,表示这块资源只能分成一份,不要分成多份,而是当成一个整体,整体申请,整体释放----整体加锁,整体解锁。
思考一下:
想要访问临界资源的前提是先申请信号量计数器资源,那么信号量计数器不也是共享资源嘛??当我们申请或者释放信号量的时候,要进行–和++,这个操作在多线程知识张杰说过不是原子的,所以在多线程的时候,可能在某一条汇编的时候就别切换走了,导致申请信号量出现问题。
申请信号量,本质是对计数器–,叫做P操作
释放信号量,本质是对计数器++,叫做V操作
正常++和–都不是原子性的,但是PV操作必须是原子的,只有一条汇编才是原子的(要么做,要么不做,没有正在做)
概念的总结:
- 信号量本质是一把计数器,PV操作,原子的。
- 执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源
- 信号量值1,0两态的叫做二元信号量,就是互斥锁功能
- 申请信号量的本质:是对临界资源的预定机制。
二、POSIX信号量
上面做了很多的铺垫,接下来我们要实现就是把之前的CP模型改成用信号量去实现的模式,我们之前的cp模型是基于阻塞队列去实现的,而现在我们是基于环形队列的cp模型,因为我们将一个整体分成许多块了,所以整个临界资源就不止一个线程在访问,所以需要下标来判断生产者和消费者线程的位置,从而使用下标的方式让他们不会访问同一个小块。之前我们学习过的环形队列,其实就是使用数组,然后对下标进行模运算,间接实现环形的效果。
来看代码实现,在CP模型的时候,写了一个任务计算器,当时没有演示,现在拿来用用:
RingQueue.hpp:
#include<iostream>
#include<semaphore.h>
#include<vector>
using namespace std;template<class T>
class RingQueue
{public: RingQueue(int cap):cap_(cap),rq_(cap){sem_init(&p_sem_,0,cap);//生产者一开始的信号量数目是capsem_init(&c_sem_,0,0);//消费者一开始的信号量数目是0pthread_mutex_init(&lock_,NULL);//给锁进行初始化。p_index_=0;c_index_=0;}void P(sem_t *sem){sem_wait(sem);}void V(sem_t *sem){sem_post(sem);}void lock(pthread_mutex_t *mutex){pthread_mutex_lock(mutex);}void unlock(pthread_mutex_t *mutex){pthread_mutex_unlock(mutex);}void push(T data){P(&p_sem_);lock(&lock_);rq_[p_index_]=data;p_index_++;p_index_=p_index_%cap_;unlock(&lock_);V(&c_sem_);}void pop(T *data){P(&c_sem_);lock(&lock_);*data=rq_[c_index_];c_index_++;c_index_=c_index_%cap_;unlock(&lock_);V(&p_sem_);}~RingQueue(){sem_destroy(&p_sem_);sem_destroy(&c_sem_);pthread_mutex_destroy(&lock_);}
private:vector<T> rq_;sem_t p_sem_;sem_t c_sem_;int p_index_;//生产者下标用来表示生产者到哪一块资源了int c_index_;pthread_mutex_t lock_;int cap_;//表示环形队列的最大长度
};
main.cc:
#include"RingQueue.hpp"
#include"task.hpp"
#include<string>
#include<unistd.h>
#include<ctime>
template<class T>
struct RQInfo
{RingQueue<T>* rq;string name;
};
void *producer_fun(void *arg)
{RQInfo<Task>* rq = static_cast<RQInfo<Task>*>(arg);string name = rq->name;RingQueue<Task>* rq_ptr = rq->rq;while(true){//生产任务int x=rand()%10+1;int y=rand()%10;char op=opers[rand()%opers.size()];Task t(x,y,op);rq_ptr->push(t);cout<<name<<" produce a task: "<<t.GetTask()<<endl;sleep(1);}return nullptr;
}void *consumer_fun(void *arg)
{RQInfo<Task>* rq = static_cast<RQInfo<Task>*>(arg);string name = rq->name;RingQueue<Task>* rq_ptr = rq->rq;while(true){Task t;rq_ptr->pop(&t);t();cout<<name<<" consume a task: "<<t.GetResult()<<endl;}return nullptr;
}
int main()
{srand(time(NULL));pthread_t producer[3],consumer[3];RQInfo<Task>* rq_info=new RQInfo<Task>();RingQueue<Task> *rq = new RingQueue<Task>(10);rq_info->rq = rq;//单生产单消费的环形CP模型。for(int i=0;i<1;i++){rq_info->name = "productor-"+to_string(i);pthread_create(producer+i,NULL,producer_fun,rq_info);sleep(1);}for(int i=0;i<1;i++){rq_info->name = "consumer-"+to_string(i);pthread_create(consumer+i,NULL,consumer_fun,rq_info);sleep(1);}for(auto tid:producer){pthread_join(tid,NULL);}for(auto tid:consumer){pthread_join(tid,NULL);}return 0;
}
测试办法:
- 先在main.c中将生产者消费者设置成单生产单消费,然后使生产者先休眠三秒,看看消费者什么状态,目的是测试生产者是不是先跑
代码设计细节:
1.通过结构体将线程名和环形队列一起当参数传进去
2.在设计一些接口时可以进行封装,我们信号量的借口和我们熟悉的PV操作不对应,封装成熟悉的
3.我们进行加锁和申请信号量的时候,有两种一种先加锁,一种先申请,哪种好??答案是先申请好,(1)信号量本身就是原子的,不需要被保护。(2)先加锁在申请是一个串行,如果先申请在加锁,就会出现一个线程在访问的时候,其他线程可以申请信号量,实现并发访问。
三、总结
如果没有学习多线程,听信号量是一件痛苦的事情,但是我们学过多线程并且还学了锁,对于信号量的理解我认为大家是没有问题的,大家下来要去联系一下代码,多去测试一些情况,看看和自己预想的是不是一样的。话不多了,这篇就讲到这里,下篇我们开始讲解线程池,希望大家到时侯过来支持。