[Linux]——基于信号量的生产者消费者模型

生产者消费者模型

上篇博客基于阻塞队列的生产者消费者模型笔者已经详细的为大家介绍了什么是生产者消费者模型,而本篇博客也是实现一个生产者消费者模型,不过这次我们将会带领大家使用信号量实现一个基于循环队列的生产者消费者模型。

POSIX信号量

小伙伴们擦亮眼睛,这里我们使用的是基于POSIX的信号量,有的同学可能会误认为是SystemV的信号量,他们两是不同的。我指的是这两个东西不一样,但是他们的作用相同,他们都用于同步操作,达到无冲突访问共享资源的目的,但是POSIX信号量可以用于线程同步。

有的同学这里最疑惑的点还是信号量到底是个什么东西,这里我们不妨就使用最简单的例子给大家说明,其是你可以简单的将信号量理解为是一个计数器,计数器为1就代表你还有一份资源,如果有人来申请到了这份资源,那么这个计数器就会减一变为0,其他人再想申请资源由于信号量为0申请者就会被挂起进去等待队列。

初始化信号量时初始值可以给大于0的非负数,表示你当前所拥有的资源数,还是不能理解的朋友这里举一个生活中的例子,各个旅游景点的移动厕所见过把(虽然有点奇怪,但是这个例子很贴切),开始假如有5个空厕所,每进去一个人数量就会减一,当最后一个厕所也被占用时,后面来的人就会排队等待,有人出来厕所数量加一,也就会唤醒等待的人。这个例子就很好的解释了信号量。

信号量有两个很重要的操作,一个是加操作,也被称为v操作,此操作会让信号量加一,另一个操作为减操作,也被称为p操作,此操作会让信号量值减一,接下来我们看看信号量的操作函数。

创建信号量

#include 
int sem_init(sem_t *sem, int pshared, unsigned int value);
//pshared:0表示线程间共享,非零表示进程间共享
//value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

名字还起的挺fashion的,然而功能就是我们所说的p操作,也就是减一

int sem_wait(sem_t *sem);

发布信号量

这个函数对应了我们的v操作,也就是让信号量加一

int sem_post(sem_t *sem);

基于循环队列的生产者消费者模型

我们这次实现的生产者消费者模型是基于循环队列的,生产者每向队列中生产一批数据,消费者就可以从队列中读取到一批数据,所以此时循环队列就变成了交易场所,p表示生产者,c表示消费者,当生产者向队列中push后消费者就可以将数据拿走,这里需要明白的一点是,生产者必须先生产后消费者才能进行消费,而且生产者不能套一个圈再次超过消费者,当队列放满时生产者就需要挂起等待消费者消费。

[Linux]——基于信号量的生产者消费者模型_第1张图片
然而现在有一个很大的问题,你怎么判断此时队列到底是空还是满呢?你会发现队列为空或者为满时p和c都指向了同一位置,根本无法判断,所以问题短暂的转化为了算法问题,不过我们其实可以使用一个计数器来记录当前数据的多少
[Linux]——基于信号量的生产者消费者模型_第2张图片
[Linux]——基于信号量的生产者消费者模型_第3张图片
哦吼,提到计数器你是不是想到了什么,没错,我们的信号量其实就是一个计数器。所以我们最终的解决的办法是使用俩个信号量来记录,其中一个信号量记录队列中空格子的数量,另外一个信号量记录队列中数据的多少。

  • 如果对队列中push数据,记录数据的信号量执行v操作,记录空格子的信号量执行p操作
  • 如果对队列中pop数据,记录数据的信号量执行p操作,记录空格子的信号量执行v操作

初始化信号量时记录空格子的信号量的大小为队列的长度,记录数据的大小为0。现在暂且不谈怎么保证生产者不会套一个圈超过消费者,和怎么保证程序开始执行时让生产者先生产,其实后面你会发现这都不是问题。我们的挑战好像远远不止这些,现在好好想想,有一个数据结构是循环队列么?没有,所以我们索性自己写一个循环队列,看起来队列好像是环状的,其实我们只不过是使用数组模拟。
[Linux]——基于信号量的生产者消费者模型_第4张图片
现在再来解决怎么保证生产者不会套一个圈超过消费者,和怎么保证程序开始执行时让生产者先生产的问题:
先来看看怎么解决让消费者先生产的问题,如下图,此时循环队列中没有数据,还记得我们记录数据的信号量么,最开始他的值为0,所以他申请资源会发生什么?没错,它会被挂起,因为当前没有数据资源可以被申请,但是当前记录空格子的信号为数组长度,这也就意味着一定是生产者先生产
[Linux]——基于信号量的生产者消费者模型_第5张图片
那么其实不被套一个圈的道理是相同的,如果队列被生产满后,pc处于相同的问题,此时能使用的空格子为0,生产者再次去申请资源就会被挂起等待消费者将数据拿走后唤醒他。到这我们所有的问题全部都解决了,那么话不多说,我们直接贴上实现的代码。

#include 
#include 
#include 
#include 
#include 
#define NUM 16

class RingQueue{
private:
	std::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); // P
		q[consume_step] = data;
		consume_step++;
		consume_step %= cap;
		sem_post(&data_sem); //V
	} 
	void GetData(int &data)
	{
		sem_wait(&data_sem);
		data = q[product_step];
		product_step++;
		product_step %= cap;
		sem_post(&space_sem);
	} 
	~RingQueue()
	{
		sem_destroy(&data_sem);
		sem_destroy(&space_sem);
	}
};

void *consumer(void *arg)
{
	RingQueue *rqp = (RingQueue*)arg;
	int data;
	for( ; ; ){
		rqp->GetData(data);
		std::cout << "Consume data done : " << data << std::endl;
		sleep(1);
	}
} 

void *producter(void *arg)
{
	RingQueue *rqp = (RingQueue*)arg;
	srand((unsigned long)time(NULL));
	for( ; ; ){
		int data = rand() % 1024;
		rqp->PutData(data);
		std::cout << "Prodoct data done: " << data << std::endl;
		// sleep(1);
	}
} 
int main()
{
	RingQueue rq;
	pthread_t c,p;
	pthread_create(&c, NULL, consumer, (void*)&rq);
	pthread_create(&p, NULL, producter, (void*)&rq);
	pthread_join(c, NULL);
	pthread_join(p, NULL);
}

此时我们就完成了基于信号量循环队列的生产消费者模型,只要读者清楚的明白上文的问题是如何解决的,那么相信对于理解上述的代码并不会有太大的问题。

总结

我们现在实现了基于阻塞队列和基于信号量的生产者消费者模型,对于他们的实现其实都不是很难,重点是读者们需要理解问题解决的办法和理解模型本身的作用和意义。

你可能感兴趣的:(Linux)