【Linux】生产者消费者模型

生产者消费者模型(一)(模拟单线程的互斥与同步)

在实现的软件开发过程中,经常会碰到如下情景:·某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程和进程等)。产生数据的模块就称之为生产者,而处理数据的模块,就是消费者。

1. 条件变量

在正式开始生产者与消费者模型之前,我们应该对条件变量有一个新的认识。

条件变量(Condition Variable):它的作用是描述资源的就绪状态,属于线程的一种同步机制。互斥锁用于上锁,条件变量则用于等待。条件变量本身是由互斥量保护的,线程在改变条件之前首先会封锁互斥量,因此其他线程在获得互斥量之前不会察觉到这样的变化。

① pthread_cond_init和pthread_cond_destroy:条件变量的初始化与销毁。
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);//条件变量销毁
int pthread_cond_init(pthread_cond_t *restrict cond,//条件变量初始化
    const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

参数cond:条件变量。

参数attr:条件变量的属性,设置为NULL则表示缺省属性,

返回值:成功返回0,失败返回错误编号。

和mutex的初始化和销毁类似,如果条件变量是静态分配的,也可以使用PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并设置参数attr为NULL。

② pthread_cond_wait/timewait:前者是条件变量等待,后者是等待超时。
 #include <pthread.h>
 int pthread_cond_timedwait(pthread_cond_t *restrict cond,
               pthread_mutex_t *restrict mutex,
 const struct timespec *restrict abstime);
 int pthread_cond_wait(pthread_cond_t *restrict cond,
               pthread_mutex_t *restrict mutex);

参数cond:条件变量。

参数mutex:互斥锁。

返回值:成功返回0,失败返回错误编号。

pthread_cond_timewait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。

可见,一个Condition Variable总是和一个mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个条件变量上阻塞等待,这个函数需要以下三步操作:

1)释放mutex 2)阻塞等待 3)当被唤醒时,重新获得mutex并返回。

③ pthread_cond_broadcast/signal:唤醒线程。前者是唤醒进程内的所有线程,后者是唤醒一个线程。
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

返回值:成功返回0,失败返回错误编号。

一个线程可以调用pthread_cond_signal唤醒在某个条件变量上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个条件变量上等待的所有线程。

2. 生产者消费者模型

生产者与消费者牵扯最多的是多线程的同步问题,单单抽象出生产者和消费者,还够不上生产者消费者模。因此该模式需要加入一个缓冲区作为二者的媒介,生产者把数据放入缓冲区,消费者从缓冲区取走数据,结构图如下:
在这里插入图片描述
生产者与消费者模型的3-2-1规则:3个关系,2个角色,1个交易场所(本篇使用链表模拟)。

后两个一目了然,我主要说一下三种关系:

① 生产者与生产者(互斥)

② 生产者与消费者(互斥+同步)

③ 消费者与消费者(互斥)

也就是说,生产者生产的时候消费者不能消费,消费者消费的时候生产者同样不能生产。缓冲区为空时,消费者不能消费,缓冲区满时,生产者也不能再生产。

3. 生产者消费者模型的三大特性

▶解耦(软件工程追求高内聚、低耦合)

假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(耦合)。将来如果消费者的代码出现变化,可能会影响到生产者。但是如果二者均依赖于同一个方法(缓冲区),耦合自然就降低了。

▶支持并发(最主要的特性)

由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只能一直等待,如果消费者的处理速度很慢,生产者则会浪费很多时间。因此,生产者消费者模型就提出了缓冲区的概念,即生产者将数据放入缓冲区后,可以继续生产,将不再依赖于消费者的处理速度。

▶支持忙闲不均

缓冲区的另一个好处在于,生产数据的速度时快时慢,当数据制造快的时候,如果消费者来不及处理,未处理的数据就可以存放进缓冲区,消费者再慢慢处理。

4. 基于单线程的生产者消费者模型

我们使用两个线程分别模拟生产者(producter)和消费者(consumer),底层数据结构借助单链表作为缓冲区,生产者通过向链表插入节点,消费者从链表中删除节点来模拟生产者与消费者的行为。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t cond = PTHREAD_MUTEX_INITIALIZER;
typedef struct list
{
	int _data;
	struct list* _next;
}Node,*pNode;
typedef struct linklist
{
	Node *phead;
}linklist,*plinklist;
pNode CreatNode(int data)
{
	pNode newNode = (pNode)malloc(sizeof(Node));
	if(NULL == newNode)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	newNode->_data = data;
	newNode->_next = NULL;
	return newNode;
}
void InitList(plinklist plist)
{
	assert(plist);
	plist->phead = NULL;
}
void Push(plinklist list,int data)
{
	assert(list);
	pNode newNode = CreatNode(data);
	if(list->phead == NULL)
	{
		list->phead = newNode;
		return;
	}
	newNode->_next = list->phead;
	list->phead = newNode;
}
void Pop(plinklist list,int* data)
{
	assert(list);
	if(list->phead == NULL)
	{
		printf("list is empty!\n");
		return ;
	}
	pNode pDel = list->phead;
	*data = pDel->_data;
	list->phead = pDel->_next;
	pDel->_next = NULL;
	free(pDel);
	pDel = NULL;
}
void Destroy(plinklist list)
{
	assert(list);
	if(list->phead != NULL)
	{
		pNode pCur = list->phead;
		while(pCur)
		{
			pNode pDel = pCur;
			pCur = pCur->_next;
			free(pDel);
			pDel = NULL;
		}
	}
	list->phead = NULL;
}
void *producter(void *arg)
{
	plinklist list = (plinklist)arg;
	while(1)
	{
		sleep(1);
		pthread_mutex_lock(&lock);//加锁
		Push(list,rand()%1000);//头插随机值
		pthread_cond_signal(&cond);//唤醒等待在该条件变量的线程
		pthread_mutex_unlock(&lock);//解锁
		printf("producter: %d\n",list->phead->_data);
	}
}
void consumer(void *arg)
{
	plinklist list = (plinklist)arg;
	while(1)
	{
		sleep(1);
		pthread_mutex_lock(&lock);//加锁
		int data = 0;
		while(list->phead == NULL)
		{
			pthread_cond_wait(&cond,&lock);//链表为空,consumer等待
		}
		Pop(list,&data);
		pthread_mutex_unlock(&lock);//解锁
		printf("consumer: %d\n",data);
	}
}
int main()
{
	linklist list;
	InitList(&list);
	pthread_t tid1,tid2;
	pthread_create(&tid1,NULL,producter,(void*)&list);//创建线程
	pthread_create(&tid2,NULL,consumer,(void*)&list);
	pthread_join(tid1,NULL);//等待线程结束回收
	pthread_join(tid2,NULL);
	Destroy(&list);
	return 0;
}

在这里插入图片描述

从结果可以看出,生产者生产一个,消费者消费一个,循环交替,实现了基于单线程的互斥与同步的生产者消费之模型。

生产者消费者模型(二)(基于环形队列)
1. 多元信号量

二元信号量是只取0值和1值的信号量

多元信号量也称为计数信号量或一般信号量。用于进程间传递信号的一个整数值。在信号量上只有三种操作可以进行,初始化、递减和增加,这三种操作均是原子操作。递减操作可以用于阻塞一个进程,增加操作可以用于接触阻塞一个进程

信号量的接口函数:

① sem_init:初始化信号量。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数sem:创建的信号量。使用sem_t可以来创建,信号量的类型为sem_t。

参数pshared:通常为0,表示信号量用于同一进程的线程间通信。

参数value:可用资源的数量。

返回值:成功返回0,失败返回错误码。(下列接口函数的返回值与此一致)

② sem_wait:获取信号量,即P操作,信号量的值减一,失败后挂起等待。
int sem_wait(sem_t *sem);
③ sem_trywait:尝试获取信号量,即P操作,信号量的值减一,失败后不挂起等待。
int sem_trywait(sem_t *sem);
 
④ sem_post:释放信号量,即V操作,信号量的值加一,同时唤醒挂起等待的线程。
int sem_post(sem_t *sem); 
⑤ sem_destroy:销毁信号量。
int sem_destroy(sem_t *sem);
2. 环形队列模拟生产者消费者模型

在上一篇博客中提出了3、2、1原则,环形队列的实现仅仅改变了这一个交易场所,由之前的单链表变为了环形队列,因此依旧属于单生产单消费的模式,这样也就不用考虑队空与队满,两个信号量针对的是队列下标为0的位置,即生产一条,消费一条。
在这里插入图片描述
对上图的解释是:n代表队列的大小,生产者关心的是格子资源blank_sem,所属区间为[n,0];消费者关系的是数据资源,所属区间为[0,n]。

//Makefile
ringQueue:ringQueue2.c
	gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
	rm -rf ringQueue
//ringQueue2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#define SIZE 64
int ring[SIZE];//环形队列
sem_t blank_sem;//格子的信号量
sem_t data_sem;//数据的信号量
//生产者
void* thread_producer(void* arg)
{
    int i = 0;
    while(1)
    {
        sleep(1);
        sem_wait(&blank_sem);//申请格子资源,格子的信号量资源-1      
		int data = rand()%10000;
        ring[i] = data;
        printf("Producer: %d\n",data);
        i++;
        i %= SIZE;
        sem_post(&data_sem);//释放数据资源,数据的信号量资源+1
    }
}
//消费者
void* thread_consumer(void* arg)
{
    int i = 0;
    while(1)
    {
        sleep(1);
        sem_wait(&data_sem); //P(data)
        printf("Consumer: %d\n",ring[i]);
        i++;
        i %= SIZE; 
        sem_post(&blank_sem);//V(blank)
    }
}
int main()
{
    sem_init(&blank_sem,0,SIZE);
    sem_init(&data_sem,0,0);
    pthread_t producer;
    pthread_t consumer;
    pthread_create(&producer,NULL,thread_producer,NULL);
    pthread_create(&consumer,NULL,thread_consumer,NULL);
    pthread_join(producer,NULL);
    pthread_join(consumer,NULL);
    sem_destroy(&blank_sem);
    sem_destroy(&data_sem);
    return 0;
}

在这里插入图片描述
使用环形队列更多的时候,是因为生产者与消费者的 速度不匹配问题,比如下面整个示例,我们让消费者晚一秒消费,为了看清楚现象,我们将队列大小改为5进行验证。
在这里插入图片描述

//ringQueue2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#define SIZE 5
int ring[SIZE];//环形队列
sem_t blank_sem;//格子的信号量
sem_t data_sem;//数据的信号量
//生产者
void* thread_producer(void* arg)
{
    int i = 0;
    while(1)
    {
        //sleep(1);
        sem_wait(&blank_sem);//申请格子资源,格子的信号量资源-1      
		int data = rand()%10000;
        ring[i] = data;
        printf("Producer: %d\n",data);
        i++;
        i %= SIZE;
        sem_post(&data_sem);//释放数据资源,数据的信号量资源+1
    }
}
//消费者
void* thread_consumer(void* arg)
{
    int i = 0;
    while(1)
    {
        sleep(1);
        sem_wait(&data_sem); //P(data)
        printf("Consumer: %d\n",ring[i]);
        i++;
        i %= SIZE; 
        sem_post(&blank_sem);//V(blank)
    }
}
int main()
{
    sem_init(&blank_sem,0,SIZE);
    sem_init(&data_sem,0,0);
    pthread_t producer;
    pthread_t consumer;
    pthread_create(&producer,NULL,thread_producer,NULL);
    pthread_create(&consumer,NULL,thread_consumer,NULL);
    pthread_join(producer,NULL);
    pthread_join(consumer,NULL);
    sem_destroy(&blank_sem);
    sem_destroy(&data_sem);
    return 0;
}

在这里插入图片描述
当我们将队列大小改为5之后就可以看清楚了,上图中生产者在一瞬间生产了5个数据,消费者再从队列里拿出数据,二者匹配处数据不一致是因为队列是先进先出,从头部拿数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农印象

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值