前言
生产者消费者模型(一)(模拟单线程的互斥与同步)
在实现的软件开发过程中,经常会碰到如下情景:·某个模块负责产生数据,这些数据由另一个模块来负责处理
(此处的模块是广义的,可以是类、函数、线程和进程等)。产生数据的模块就称之为生产者,而处理数据的模块,就是消费者。
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个数据,消费者再从队列里拿出数据,二者匹配处数据不一致是因为队列是先进先出,从头部拿数据。