生产者消费者模型
在上一篇博客[Linux]------基于阻塞队列的生产者消费者模型中,我们实现了单生产者消费者模型,在本篇博客中,我们的生产者消费者模型会有两个改变
- 不是用一般队列作为缓冲区,而是用环形队列作为缓冲区
- 不使用pthread_mutex,而使用信号量来实现生产者消费者的同步。
POSIX信号量
使用的是POSIX信号量而不是之前的system_v,因为这两个信号量作用相同,都用于同步操作,但POSIX可以用于线程间同步。
这里可以暂时把信号量当成一个计数器,当值未1时代表,有一份资源可以使用,有人申请这份资源时,值会减1,当值为0时,,其他人想继续申请这份资源,会被挂起放入等待队列中。
初始化信号量,在参数中可以传入非0的值n,代表有n个资源。
信号量有两个重要的操作,PV操作,P操作,释放资源,会使资源数+1,V操作,申请资源,会使资源数减1,下面是POSIX的具体函数。
POSIX函数
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非0表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
int sem_wait(sem_t *sem);
功能:等待信号量,将信号量的值减1
发布信号量
int sem_post(sem_t *sem);
功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量+1
循环队列
和普通队列不同,循环队列首尾相接,生产者P向队列中不断插入数据,当插入满了后,消费者C从队列中读取数据,当消费者C处理完数据后队列成空,生产者P又在队列中插入数据。
但是有一个问题出现了,当P和V同时指向一个位置时,你无法判断是队列是空还是满(这里暂时不考虑,P比C多走了n圈的可能)
上面的信号量解决了这个问题,信号量相当于计数器,所以可以设置两个信号量,一个记录队列里插入数据的数量,另一个信号量记录队列里未插入的空间的数量。
- 如果对队列进行插入数据操作,记录插入数的信号量执行P操作,记录剩余空间的信号量执行V操作。
- 如果对队列进行删除数据操作,记录插入数的信号量执行V操作,记录剩余空间的信号量执行P操作。
实际上我们可以用一个信号量,判断当两个指针指向同一片地址时是满还是空,为什么需要两个信号?
原因,假如只有一个信号量,则只能对一个对象进行限制,假设信号量记录未插入的空间记录为0时,消费者阻塞,生产者生产,但是buffer时有限的,但是没有信号量记录插入的数已经使队列满了,换句话说生产者的生产行为是不受限制的,不满足生产者消费者模型。
解决完了判断队列满或空后,还有第二个问题,在STL中没有环形队列,需要我们自己实现,这个难不倒我们,可以利用哈希表的开散列思想,用数组作为环形队列,对插入的地址进行取模,作为数组下标。
超过数组的长度,进行取模操作
还剩下最后一个问题,如何确保,生产者和消费在重合后最多差距为1圈。
因为开始的时候记录的空格子信号量计算为最大,记录插入数据的信号量为0。
因此,对于消费者申请资源会被挂起,这样生产者一定会先进行生产,假设生产者的生产速度大于消费者的处理数据的速度,当生产者把循环队列插满后,录的空格子信号量计算为0,当生产者进行下次生产,申请空的资源也会被挂起,直到,记录的空格子信号量不为0为止,这样就保证了生产者和消费在重合后最多差距为1圈。
下面是根据这种思想,写出的单生产者消费者模型。
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
using namespace std;
#define NUM 8
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):cap(_cap),q(_cap)
{
sem_init(&data_sem, 0, 0);
sem_init(&space_sem, 0, cap);
consume_step = 0;
product_step = 0;
}
~RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
}
void PutData(const int& data)
{
sem_wait(&space_sem);
q[consume_step] = data;
consume_step++;
consume_step %= cap;
sem_post(&data_sem);
}
void GetData(int & data)
{
sem_wait(&data_sem);
data = q[product_step] ;
product_step++;
product_step %= cap;
sem_post(&space_sem);
}
};
void* producter(void* arg)
{
int data;
srand((unsigned long)time(NULL));
for(;;)
{
RingQueue *bq = (RingQueue*) arg;
data = rand()%1024;
bq->PutData(data);
cout<<"the data is push done :"<<data<<endl;
// sleep(1);生产者去掉限制
}
}
void* consumer(void *arg)
{
RingQueue *bq = (RingQueue*) arg;
int data;
for(;;)
{ bq->GetData(data);
cout<<"the data is pop done :"<<data<<endl;
sleep(1);
}
}
int main()
{
RingQueue bd;
pthread_t c,p;
pthread_create(&c,NULL,consumer,(void*)&bd);//线程为可分离的
pthread_create(&p,NULL,producter,(void*)&bd);
pthread_join(c,NULL);
pthread_join(p,NULL);
}
上面我们使用了环形缓冲区来代替普通缓冲区,环形缓冲区队列相对于普通缓冲区有哪些优点?
环形缓冲区所有的push和pop操作都是在一个固定的存储空间内进行。而队列缓冲区在push的时候,可能会分配存储空间用于存储新元素;在pop时,可能会释放废弃元素的存储空间。所以环形方式相比队列方式,少掉了对于缓冲区元素所用存储空间的分配、释放。这是环形缓冲区的一个主要优势。
总结
- 基于信号量的生产者消费者模型利用信号对生产者消费者进行制约。
- 环形缓冲区相对于队列方式减少了对缓冲元素所用的储存空间的分配,释放,这是环形缓冲区的主要优势。
思想和基于阻塞队列的生产者消费者模型没有大的区别,读者应该理解生产者消费者模型的意义和解决问题的方法。