前面讲了线程之前的各种关系,以及它的概念之类的东西,下面就介绍一种特殊的线程间的关系,即生产者消费者模型,它是将实际开发过程中的数据产生模块和处理模块形象的用生产者消费者来表示。
但是仅有生产者和消费者还是不够的,还需要一块缓冲区来作为媒介,举 一个寄信的例子(虽说这年头寄信已经不时兴,但这个例 子还是 比较贴切的)。假设你要寄 一封平信,大致过程如下:1、你把信写好——相当于 生产者制造数据2、你把信放 入邮筒——相当于 生产者把数据放入缓冲区3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
优点
那么这个缓冲区有什么用呢?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?
大概有如下 一些好处。
◇解耦
假设生产者和消费者分别是两个类。如果让 生产者直接调用消费者的某个 方法,那么 生产者对于消费者就会产 生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到 生产者。 而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
接着上述的例 子,如果不使 用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有 人假冒,就惨了)。这就产 生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换 人了,你还要重新认识 一下(相当于消费者变化导致修改 生产者代码)。 而邮筒相对来说 比较固定,你依赖它的成本就 比较低(相当于和缓冲区之间的弱耦合)。
◇ 支持并发(concurrency)
生产者直接调 用消费者的某个 方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的 方法没有返回之前, 生产者只好 一直等在那边。万 一消费者处理数据很慢, 生产者就会 白 白糟蹋 大好时光。
使 用了 生产者/消费者模式之后, 生产者和消费者可以是两个独 立的并发主体(常见并发类型有进程和线程两种,后 面的帖 子会讲两种并发类型下的应 用)。 生产者把制造出来的数据往缓冲区一丢,就可以再去 生产下 一个数据。基本上不 用依赖消费者的处理速度。
其实当初这个模式,主要就是 用来处理并发问题的。
从寄信的例 子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种 方法,都挺 土的。
◇ 支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例 子来说事。假设邮递员 一次只能带走1000封信。万 一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上 用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
其实整个过程都遵循三二一原则:
即三种关系(生产者与生产者之间互斥关系,消费者与消费者之间是互斥关系,生产者与消费者之间是同步互斥关系)
两个对象(生产者与消费者)
一个交易场所(缓冲区)
下面就以实例的代码来说明问题。
单链表模拟场景
:
/*************************************************************************
> File Name: mycp.c
> Author: Maple
> Mail: rendengbin_123@163.com
> Created Time: 2017年06月25日 星期日 14时22分13秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#pragma warning (disable:4996)
typedef int DataType;
typedef struct ListNode
{
DataType data;
struct ListNode* next;
}ListNode;
ListNode* List= NULL;
pthread_mutex_t lock =PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
ListNode* BuyNode(DataType x)
{
ListNode *ptr;
ptr = (ListNode*)malloc(sizeof(ListNode));
if(ptr == NULL)
{
perror("malloc");
exit(1);
}
ptr->data = x;
ptr->next = NULL;
return ptr;
}
void PrintList(ListNode* pList)
{
if(NULL == pList)
return;
else
{
while(pList != NULL)
{
printf("%d",pList->data);
pList = pList->next;
}
}
printf("\n");
}
void PushFront(ListNode** ppList,DataType x)
{
if(ppList == NULL)
*ppList = BuyNode(x);
else
{
ListNode* tmp = BuyNode(x);
tmp->next = (*ppList);
*ppList = tmp;
}
}
void PopFront(ListNode** ppList,int* out)
{
if((*ppList)->next ==NULL)
return;
else
{
ListNode* tmp = (*ppList)->next;
(*ppList)->next = tmp->next;
*out = tmp->data;
free(tmp);
}
}
void destroyList(ListNode **pList)
{
if(*pList == NULL)
return;
while(*pList != NULL)
{
ListNode *tmp = NULL;
tmp = (*pList)->next;
free(*pList);
*pList = tmp;
}
}
void* consume(void* arg)
{
usleep(100)
int c = 0;
while(1)
{
c = -1;
pthread_mutex_lock(&lock);
while(List->next == NULL)
{
printf("consumer begin waiting\n");
pthread_cond_wait(&cond,&lock);
}
PopFront(&List,&c);
pthread_mutex_unlock(&lock);
printf("consumer is done:%d\n",c);
}
}
void* product(void* arg)
{
sleep(1);
while(1)
{
int num = rand()%1234;
pthread_mutex_lock(&lock);
PushFront(&List,num);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);
printf("product is done:%d\n",num);
}
}
int main()
{
pthread_t c,p;
pthread_create(&c,NULL,consume,NULL);
pthread_create(&p,NULL,product,NULL);
pthread_join(c,NULL);
pthread_join(p,NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
destroyList(&List);
return 0;
}
往单链表中插入元素相当于生产者,释放一个元素相当于消费者。
我们可以来讨论下面几种情景:
1、生产者速度大于消费者
2、生产者速度小于消费者
虽然存在I/O会让计算机速度大大下降,但一秒之内生产太多了,以至于无法观测到消费者开始消费的界面,所以这里我让消费者沉睡了100微秒。
可以看到,生产者100微妙内生产了大量个数据,当消费者醒来后就开始消费这些数据,那么如果生产的速度小于消费的速度呢?那就看下面的例子。
让生产者每隔一秒生产一次,消费者一直消费。
看到上面现实消费者开始等,是不是觉得和开始说缓冲的地方自相矛盾(前面说使用缓冲区及时为了避免等待),这里的等是我为了程序结果看的更明显而加入的,事实上这段时间内消费者可以做自己的事情,直到生产者告诉它有数据了。
上面就是关于单链表实现的部分,读者可以自行调整生产者和消费者之间的关系来进行测试。
循环队列实现
我们既可以通过单链表的方法来实现生产者消费者模型,还可以通过循环队列的方式。
但是有几点需要特别注意。
1、消费者必须跟在生产者后面
2、生产者不能将消费者套圈(套圈后第一圈的数据即被覆盖掉)
但是有没有注意到一点就是图中标记起点的位置,它既可能是刚开始,也可能是生产者刚准备将消费者套圈时。这里用到了信号量,可以参考信号量的有关知识。
/*************************************************************************
> File Name: mycpQueue.c
> Author: Maple
> Mail: rendengbin_123@163.com
> Created Time: 2017年06月29日 星期四 14时55分51秒
************************************************************************/
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int arr[64];
sem_t semdata;
sem_t semblank;
void *consum(void *arg)
{
int step = 0;
while(1)
{
sleep(1);
sem_wait(&semdata);
int data = arr[step];
step++;
step %= 64;
printf("consumer:%d\n",data);
}
return NULL;
}
void *produce(void *arg)
{
int step = 0;
while(1)
{
//sleep(2);
sem_wait(&semblank);
int data = rand()%1234;
arr[step] = data;
step++;
step %= 64;
printf("Producer:%d\n",data);
sem_post(&semdata);
}
return NULL;
}
int main()
{
sem_init(&semblank,0,64);
sem_init(&semdata,0,0);
pthread_t c,p;
pthread_create(&c,NULL,produce,NULL);
pthread_create(&p,NULL,consum,NULL);
pthread_join(c,NULL);
pthread_join(p,NULL);
sem_destroy(&semblank);
sem_destroy(&semdata);
return 0;
}
当生产者快与消费者时:
而当消费者快于生产者时:
这就是生产者消费者的模型,下面我们来介绍另外一种模型。