1 🍑POSIX信号量 🍑
我们可以先来回忆回忆下什么是信号量?
信号量本质是一种计数器,是对资源的一种预订机制,也就是说只要你信号量申请成功了,那么就说明该资源在某个时刻一定是属于你的,不会有别的线程来占有。这个听起来是不是有写熟悉呀?感觉有点儿像互斥锁,其实互斥锁本身就是一种二元信号量,而信号量是可以划分成多元的,也就是说资源可以被分成了多份,我们可以申请这些被分成的小资源。
说到信号量,就不得不提到PV操作,P操作是申请资源,V操作是释放资源。(这个不知道的可以自行下去百度下,很快就能够理解)
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
2 🍑信号量的接口介绍 🍑
2.1 🍎初始化信号量🍎
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值
2.2 🍎销毁信号量🍎
int sem_destroy(sem_t *sem);
2.3 🍎等待信号量🍎
功能:等待信号量,会将信号量的值减1 int sem_wait(sem_t *sem); //P()
2.4 🍎发布信号量🍎
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V()
3 🍑基于环形队列的生产者消费者模型 🍑
3.1 🍎环形队列🍎
在上篇文章讲解生产者消费者模型的时候,我们使用的是阻塞队列来帮助我们实现的。现在我们要换一种方式来实现生产者消费者模型,也就是使用循环队列来帮助我们实现。
什么是循环队列呢?其实并不一定就是一个队列,较为简单是直接用数组模拟实现出来的,比如下面就是一个较为简单的循环队列模型,我们可以将一个大的资源,划分成为了一个一个的小资源,通过对小资源的同步和互斥要求来达到实现生产者消费者模型的目的:
但是现在问题来了,我们如何指定规则让环形队列构建生产者消费者模型呢?
我们不妨构建一个生产者和一个消费者,刚开始让生产者和消费者指向同一个位置,然后让生产者先跑,消费者在后面追,注意生产者先跑了后,消费者和生产者可以同时跑,但是必须要满足下面的规则:
不能让生产者套圈消费者
不能让消费者超过生产者
当满足上面规则后我们就能够完成一个生产者消费者的模型了。
特别注意:
- 当环形队列为空时,应该让生产者先运行(同步)
- 当环形队列为满时,应该让消费者先运行(同步)
- 其余情况时生产者消费者可以并发运行
3.2 🍎基于环形队列的单生产者单消费者模型代码实现🍎
在讲解代码实现前我们应该先想想环形队列的成员应该有哪些?
首先来说,我们可以用一个vector模拟队列,记录一个容器大小的整形变量,除此之外因为生产者只关心空间资源,所以我们还得加一个空间资源信号量_space_sem,同理,消费者只关心数据资源,所以还要加一个数据资源信号量_data_sem,但是我们push和pop数据方便我们还得加上当前生产者与消费者的位置_p_idx和_c _idx有了这些理解后我们就可以上手撸代码了:
#pragma once #include<iostream> #include<vector> #include<pthread.h> #include <semaphore.h> const int N=5; using namespace std; template<class T> class ringQueue { public: ringQueue(size_t num=N) :_cap(num) ,_q(num) { sem_init(&_space_sem,0,num); sem_init(&_data_sem,0,0); _p_idx=_c_idx=0; } void P(sem_t* psem) { sem_wait(psem); } void V(sem_t* psem) { sem_post(psem); } void push(const T& data)//生产者push数据,消耗了空间资源,增加了数据资源 { P(&_space_sem); _q[_p_idx++]=data; _p_idx%=_cap; V(&_data_sem); } void pop(T& data)//消费者pop数据,消耗了数据资源,增加了空间资源 { P(&_data_sem); data=_q[_c_idx++]; _c_idx%=_cap; V(&_space_sem); } ~ringQueue() { sem_destroy(&_space_sem); sem_destroy(&_data_sem); } private: vector<T> _q; size_t _cap; sem_t _space_sem;//生产者关心空间资源 sem_t _data_sem;//消费者关心数据资源 int _p_idx;//生产位置 int _c_idx;//消费位置 };
从上面的代码中我们看到:与之前讲解阻塞队列的情况有些不太一样,因为我们并没有在里面加锁,为什么呢?
原因其实很简单,因为我们在实现线程库给我们提供的wait操作时如果条件不满足会阻塞在那里,直至条件满足;并且我们生产者与消费者在非空和非满的情况下是可以并发运行的;我们实现的是单生产者单消费者模型不会存在多个生产者或者多个消费者共同抢占同一份资源的情况。
我们可以下一个测试程序来看看:
#include<iostream> #include<time.h> #include<unistd.h> #include"RingQueue.hpp" using namespace std; void* pRun(void* args) { ringQueue<int>* prq=static_cast<ringQueue<int>* >(args); while(true) { sleep(1); int data=rand()%10+1; prq->push(data); cout<<"productor make "<<data<<endl; } return nullptr; } void* cRun(void* args) { ringQueue<int>* prq=static_cast<ringQueue<int>* >(args); while(true) { int data; prq->pop(data); cout<<"consumer deal "<<data<<endl; } return nullptr; } int main() { srand((size_t)time(nullptr)); ringQueue<int>* prq=new ringQueue<int>(5); pthread_t p,c; pthread_create(&p,nullptr,pRun,prq); pthread_create(&c,nullptr,cRun,prq); pthread_join(p,nullptr); pthread_join(c,nullptr); delete prq; return 0; }
当我们运行时:
至于打印的结果有时候看着是乱的是因为我们在准备任务资源以及处理任务是可以并发进行的。
3.3 🍎基于环形队列的多生产者多消费者模型代码实现🍎
大家想想看,上面的代码能够完成多生产者多消费者的场景吗?
仔细思考下,我们发现其实是不能够的。因为在多生产者多消费者在多线程的情况下是可以并发的抢占资源的,而我们更新资源的代码并没有加锁,所以是会造成线程安全的问题。
那我们应该加几把锁呢?
答案肯定是两把,注意不能够只加一把锁。因为我们是不能够让多个生产者线程共同进入临界区,也不能够让多个消费者线程共同进入临界区,但是生产者和消费者在某些情况下是可以并发进入临界区的。
所以我们就不妨来改改代码:
#pragma once #include<iostream> #include<vector> #include<pthread.h> #include <semaphore.h> const int N=5; using namespace std; template<class T> class ringQueue { public: ringQueue(size_t num=N) :_cap(num) ,_q(num) { sem_init(&_space_sem,0,num); sem_init(&_data_sem,0,0); _p_idx=_c_idx=0; pthread_mutex_init(&_p_mutex,nullptr); pthread_mutex_init(&_c_mutex,nullptr); } void P(sem_t* psem) { sem_wait(psem); } void V(sem_t* psem) { sem_post(psem); } void push(const T& data)//生产者push数据,消耗了空间资源,增加了数据资源 { pthread_mutex_lock(&_p_mutex); P(&_space_sem); _q[_p_idx++]=data; _p_idx%=_cap; V(&_data_sem); pthread_mutex_unlock(&_p_mutex); } void pop(T& data)//消费者pop数据,消耗了数据资源,增加了空间资源 { pthread_mutex_lock(&_c_mutex); P(&_data_sem); data=_q[_c_idx++]; _c_idx%=_cap; V(&_space_sem); pthread_mutex_unlock(&_c_mutex); } ~ringQueue() { sem_destroy(&_space_sem); sem_destroy(&_data_sem); pthread_mutex_destroy(&_p_mutex); pthread_mutex_destroy(&_c_mutex); } private: vector<T> _q; size_t _cap; sem_t _space_sem;//生产者关心空间资源 sem_t _data_sem;//消费者关心数据资源 int _p_idx;//生产位置 int _c_idx;//消费位置 pthread_mutex_t _p_mutex;//防止多个生产者共同进入临界区 pthread_mutex_t _c_mutex;//防止多个消费者共同进入临界区 };
但是上面的代码还有一个小地方还可以优化,我们来看看这里:
我们是把加锁放在了P操作之前,这样做有必要吗?我们想想,其实这种是没有必要的,因为P操作的实现本身就是原子操作,所以不可能申请到同一份资源,如何我们将其放在锁的外面,其实还可以多线程并发的申请不同的资源,提高效率;同理V操作释放资源因该放在临界区之外。所以我们极力推荐下面这种写法: