一、什么是生产者与消费者模型
在实际的生活中我们可能会遇到下面这种情形,一个模块负责产生数据,另一个模块负责处理数据。用来产生数据的模块成为生产者,用来处理数据的模块被成为消费者。只有生产者和消费者还不够,这个模型还必须要有一个缓冲区处于生产者和消费者之间,作为中介。生产者把数据放入缓冲区,而消费者从缓冲区中取出数据。
二、生产者与消费者模型的条件和用途
用一句话来说明生产者和消费者之间的关系,那就是“三、二、一原则”仔细来说的就是“三种关系、两类对象、一个交易场所”
三种关系:生产者和消费者
两类对象:三种关系分别就是:生产者和消费者(互斥和同步)、生产者和生产者(互斥)、消费者和消费者(互斥)
一个交易场所:交易场所是生产者和消费者进行数据交换的仓库,相当于一个缓冲区,生产者负责把数据放到缓冲区中去,而消费者则就是把缓冲区中的数据拿出来进行处理。
生产者和消费者之间特点:
1、生产者只会关心缓冲区,不需要关心具体的消费者的情况。
2、对于消费者而言,不需要关心具体的生产者它也是只需要关心,缓冲区中还有没有数据在。
3、生产者生产的时候消费者不能进行消费,消费者进行消费的时候生产者不能进行生产。相当于是一种互斥关系,也就是生产者和消费者同时只能有一个访问缓冲区。
4、缓冲区为空的时候不能进行消费。
5、缓冲区满的时候不能进行生产。
三、为什么要使用生产者消费者模型?
1、支持解耦
假设生产者和消费者是两个类,如果让生产者直接调用消费者的某个函数,那么生产者和消费者之间就会产生依赖(耦合)。如果消费者的代码发生变化可能会影响到生产者,而如果两者都依赖某个缓冲区,两者之间不直接依赖,耦合性也就降低了。
2、支持并发
生产者调用消费者的某个方法有一个弊端,就是函数调用时同步的(或者称作阻塞)在消费者的方法没有返回之前生产者只好一直阻塞。如果消费者处理数据很慢,那么生产者就会白白浪费时间。使用生产者消费者模式之后,生产者消费者就是两个独立的主体。生产者把制造出的数据放到缓冲区就可以生产下一个数据。
3、支持忙闲不均
缓冲区还有一个好处就是如果产生数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造的快那么数据来不及处理的时候,就可以将数据暂时存放在缓冲区,当数据生产慢的时候就可以继续使用。
四、什么是条件变量
上面讲的是同步与互斥来实现生产者和消费者模型,我们还要解决一个问题就是:假如你去商场买东西,发现货架是空的,超市并没有提供任何商品给你买。那你会不会很失望? 为了避免这种事情的发生,我们就需要条件变量来帮助我们。当有商品时,会通知你去购买,当没有商品时,就一直等待。
条件变量(condition variable):
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”;另一个线程使“条件成立”(给出条件成信号)。所以条件变量使我们可以睡眠等待某种条件出现。
为了防止竞争,条件变量的使用总是和一个互斥锁进行搭配。
五、条件变量的用法
条件变量成功就返回0,失败就返回的是错误码。
条件变量的数据类型:
pthread_cond_t
条件变量的初始化:
1、直接定义一个全局的条件变量,并利 PTHREAD_COND_INITIALIZER进行值得初始化。
2、调用函数pthread_cond_init进行初始化。
pthread_cond_t (pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
第一个参数即为我们动态分配的条件变量cond,除非创建一个非默认属性的条件变量,否则第二个参数attr始终为NULL;
注意:不想将条件变量定义成全局的,必须以动态分配的方式创建
pthread_cond_t *cond = (pthread_cond_t*)malloc(sizeof(pthread_cond_t));
条件变量的销毁:调用pthread_cond_destroy函数销毁条件变量
条件变量的等待:
使用pthread_cond_wait或pthread_cond_timewait函数等待条件变量变为真
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);
pthread _cond_wait:
第一个参数是指向条件变量的指针,第二个参数是一个指向互斥锁的指针。条件变量总是和一把互斥锁协同使用的。目的是为了防止资源的竞争。wait函数的第二个参数就是为了当消费者在申请到锁,但是条件变量为假(没有锁资源)的情况下及时的释放锁资源。
生产者与消费者之间是互斥关系。不能同时访问交易场所,所以我们加一把锁来协同两者。
栗子:
假设此时有两个消费者A,B都在等待资源(等待条件变量变为真)。此时生产者申请到了”锁”,并且生产了一个“商品”。紧接着释放了锁资源,并发送信号告诉消费者:“你们可以来买了”。
消费者A率先抢到了锁,并且买走了这个“商品”。之后消费者B申请到了锁,进入商场发现已经没有商品了,此时条件变量为假。消费者B会一直等到条件变量为真。但是此时锁在B身上,如果B不释放锁,并且一直等待的话,生产者因为申请不到锁而无法生产“商品”,消费者B因为没有商品而一直等待。就会造成“死锁”问题。
六、生产者消费者模型的实例
生产者消费者的例子就是,生产者生产一个数据存放在一个链表的头上,依次放在头上。消费者每次都从链表的头上进行取数据。具体代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
typedef struct Node
{
int data;
struct Node* next;
}Node,*Node_p,**Node_pp;
Node_p CreatNode(int data)
{
Node_p _n = (Node_p)malloc(sizeof(Node));
if(_n == NULL)
{
return NULL;
}
_n->data = data;
_n->next = NULL;
return _n;
}
void Init(Node_pp list)
{
*list = CreatNode(0);
}
void PushFront(Node_p list,int data)
{
assert(list);
Node_p _n = CreatNode(data);
_n->next = list->next;
list->next = _n;
}
void DelNode(Node_p node)
{
assert(node);
free(node);
node = NULL;
}
int Isempty(Node_p list)
{
assert(list);
if(list->next != NULL)
{
return 0;
}
{
return 1;
}
}
void PopFront(Node_p list,int *data)
{
assert(data);
if(Isempty(list))
{
printf("The list is empty!\n");
return;
}
Node_p _del = list->next;
list->next = _del->next;
*data = _del->data;
DelNode(_del);
}
void Destory(Node_p list)
{
int data = 0;
assert(list);
while(list->next)
{
PopFront(list,&data);
}
free(list);
list = NULL;
printf("list if destroy..\n");
}
void Print(Node_p list)
{
assert(list);
Node_p cur = list->next;
while(cur)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("\n");
}
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond = PTHREAD_COND_INITIALIZER;
void* Consumer(void *arg)
{
Node_p head = (Node_p)arg;
while(1)
{
int data = 0;
pthread_mutex_lock(&mylock);
if(Isempty(head))
{
pthread_cond_wait(&mycond,&mylock);
}
PopFront(head,&data);
printf("consumer: %d\n",data);
pthread_mutex_unlock(&mylock);
}
return NULL;
}
void* Producer(void *arg)
{
Node_p head = (Node_p)arg;
while(1)
{
usleep(123456);
int data = rand()%1000;
pthread_mutex_lock(&mylock);
PushFront(head,data);
printf("Producer: %d\n",data);
pthread_cond_signal(&mycond);
pthread_mutex_unlock(&mylock);
}
return NULL;
}
int main()
{
Node* head = NULL;
Init(&head);
pthread_t tid1,tid2;
int ret1 = pthread_create(&tid1,NULL,Consumer,(void*)head);
int ret2 = pthread_create(&tid2,NULL,Producer,(void*)head);
printf("ret1:%d,ret2:%d\n",ret1,ret2);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mylock);
pthread_cond_destroy(&mycond);
return 0;
}
七:信号量
我们除了可以用上面的那种方法进行实现生产者和消费者模型之外还可以用信号量来实现。假如,商场的货架只有五个摆放存放商品的格子。每个格子只能放一件商品。这五个格子可以循环利用。可以把其想成一个环形队列。
生产者与消费者在环形队列下需要遵循以下规则:生产者优先,消费者的永远不能追上生产者,生产者也不能超过消费者一圈。(可以想象成两个人赛跑)
生产者关注的是格子资源,消费者关注的是商品资源。我们就可以定义两个信号量来分别代表这两个资源。
信号量的数据类型为sem_t,函数接口的头文件为semaphore.h
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
八:信号量来实现生产者消费者模型
要让信号量来代替互斥锁和条件变量实现生产者和消费者模型,我们需要把数据现在存在一个环形对列里面来进行实现。
代码如下:
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int ring[64]; //环形队列
sem_t blank; //空格 表示空格的信号量
sem_t datas; //数据 表示数据的信号量
void* Producer(void* arg)
{
int i = 0;
while(1)
{
sleep(1);
sem_wait(&blank);
int data = rand()%10000;
ring[i] = data;
i++;
i %= 64;
printf("produce: %d\n",data);
sem_post(&datas);
}
}
void* Consumer(void* arg)
{
int i = 0;
while(1)
{
sleep(1);
sem_wait(&datas);
int data = ring[i];
i++;
i %= 64;
printf("consumer: %d\n",data);
sem_post(&blank);
}
}
int main()
{
sem_init(&blank,0,64);
sem_init(&datas,0,0);
pthread_t producer;
pthread_t consumer;
pthread_create(&producer,NULL,Producer,NULL);
pthread_create(&consumer,NULL,Consumer,NULL);
pthread_join(producer,NULL);
pthread_join(consumer,NULL);
sem_destroy(&blank);
sem_destroy(&datas);
return 0;
}