1.环形缓冲区的优势
在上篇博客基于阻塞队列的生产者消费者模型中我介绍了什么是生产者消费者模型以及生产者-消费者模式,还没了解的可以戳链接查看。
基于阻塞队列的实现,虽然简单,但是对内存分配性能要求较高的程序是不适用的,因为该方式在push的时候,可能会分配存储空间用于存储新元素;在pop时,可能会释放废弃元素的存储空间。如此频繁的对缓冲区进行分配和释放会大大浪费使用该模式的性能。
本篇博客是提高内存分配性能的一个解决方案-环形队列,在指定环形队列的大小后,我们可以将所有的push和pop操作都在这个固定的存储空间内进行,因此少掉了对于缓冲区元素所用存储空间的分配、释放。这是环形缓冲区的一个主要优势。
2.POSIX 信号量
这里要注意了,我们这次使用的是基于POSIX的信号量,千万不要误认为是SystemV的信号量,虽然两个东西不一样,但是他们的作用相同,他们都是用于保证同步的操作,达到无冲突访问共享资源的目的,但是POSIX信号量可以用于线程同步。
信号量本质上是一个记录临界资源数目的计数器,它有两个很重要的操作,一个是加操作,也被称为v操作,此操作会让信号量加一,另一个操作为减操作,也被称为p操作,此操作会让信号量值减一,接下来我们看看信号量的操作函数。
这里需要特别注意一点:虽然它只是是一个计数器,但是不能用全局变量代替,因为信号量的PV操作是具有原子性的,而我们自己定义的全局变量无法保证原子性。
信号量的相关操作
- 初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
sem:信号量
//pshared:0表示线程间共享,非零表示进程间共享
//value:信号量初始值
- 摧毁信号量
int sem_destroy(sem_t *sem);
- 等待信号量
int sem_wait(sem_t *sem);
功能:等到信号量时会将信号量的值减一,对应P操作
- 发布信号量
int sem_post(sem_t *sem);
功能:资源使用结束后,归还资源将信号量的值加一,对应V操作
3.基于环形队列实现的生产消费模型的原理
上节我们基于阻塞队列实现的生成消费模型中,其空间大小可以进行动态分配。
前面分析得,如果要提高内存分配的性能,我们需要使用固定大小的环形队列来实现这个模型。
生产者每向队列中生产一批数据,消费者就可以从队列中读取到一批数据,所以此时循环队列就变成了交易场所,p表示生产者,c表示消费者,当生产者向队列中push后消费者就可以开始把数据拿走。
这里有几个原则需要遵循:
- 消费行为不能先于生产行为
- 生产行为不能超过消费行为一圈
- 虽然空 / 满的状态不好确定,但它俩一定是在同一点
- 队列为空时只能有生产行为,为满时只能有消费行为
现在我们模拟实现多线程间的同步过程有下面几个点需要关注:
- 生产者(P)的操作:取空位增数据
P(sem_space)空位个数- -
V(sem_data)数据个数+ +
- 消费者(C)的操作:取数据增空位
P ( sem_data ) 数据个数- -
V(sem_space) 空位个数+ +
- 元素存储
由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储值类型的数据,而不要存储指针(引用)类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。
- 判断空&满
环形结构的其实状态和结束状态都是一样的,不好判断为空还是未满,所以我们通过加计数器或者标志位来判断,也可以通过预留一个空的位置作为满的状态。
不过我们现在有信号量这个计数器,所以我们用数组moni 环形队列,用模运算模拟环形的特性。
4. 代码实现
如上内容都理解后,我们就可以实现该模型了。话不多说,看下面的代码:
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <time.h>
#define NUM 16
using namespace std;
class RingQueue{
private:
vector<int> q; //模拟环形对列数组
int cap; //资源的数量
sem_t data_sem; //数据
sem_t space_sem; //空位
int consume_step; //消费者所在位置
int product_step; //生产者所在位置
public:
RingQueue(int _cap=NUM):q(_cap),cap(_cap)
{
sem_init(&data_sem,0,0);
sem_init(&space_sem,0,cap);
consume_step=0;
product_step=0;
}
void PutData(const int &data) //生产者行为
{
sem_wait(&space_sem); //等待信号对应减一
q[product_step]=data;
product_step++;
product_step%=cap; //保证不越界
sem_post(&data_sem); //发布信号对应加一
}
void GetData(int &data) //消费者行为
{
sem_wait(&data_sem); //等待数据
data=q[consume_step];
consume_step++;
consume_step%=cap;
sem_post(&space_sem); //发布空位
}
~RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
}
};
void *consume(void *arg)
{
RingQueue *rq=(RingQueue*)arg;
int data;
while(1)
{
rq->GetData(data);
cout<<"Consume data done:"<<data<<endl;
sleep(1);
}
}
void *product(void *arg)
{
RingQueue *rq=(RingQueue*)arg;
srand((unsigned long)time(NULL));
while(1)
{
int data=rand()%100+1;
rq->PutData(data);
cout<<"Product data done:"<<data<<endl;
//sleep(1);
}
}
int main()
{
RingQueue rq;
pthread_t c,p;
pthread_create(&c,NULL,consume,(void*)&rq);
pthread_create(&p,NULL,product,(void*)&rq);
pthread_join(c,NULL);
pthread_join(p,NULL);
return 0;
}
运行结果:
相信上面的代码是很容易理解的。通过基于阻塞队列和基于信号量的环形队列实现生产者消费者模型,我们发现实现起来并不是很难,重点是读者们需要理解问题解决的办法和理解模型本身的作用和意义。
下面有篇文章写的非常不错,可以进一步学习参考~
生产者/消费者模式