生产者消费者模型
谈生产者消费者模型之前我们必须知道什么是生产者消费者模型,看题目你可能觉得他是一个十分高深的东西,但是或许我们身边就存在生产者消费者模型。
概念引入
打个比方,生活中我们经常在缺少生活用品或者其他商品的时候我们通常会选择去超市,那也就不难理解,我们其实就是所谓的消费者。那生产者是超市么?答案是否定的,如果超市是生产者,那供货商是什么,所以供货商是所谓的生产者。再谈超市,其实超市是为了生产者和消费者交易而诞生的,所以超市就是交易场所。
将场景引入到我们的软件开发中来,不难类比,一个生产数据的模块就是生产者,处理这些数据的模块可以认为是消费者,生产者生产的数据不直接交付给消费者,而是放入某个缓冲区,让消费者自己来取,那么这个缓冲区其实就可以认为是交易场所。
综上,一个生产者消费者模型右三部分构成,生产资,消费者,交易场所。他们之间的关系就如同下图。
为什么需要它?
相信身为程序员的你每天都会做一件事,而且日复一日,那就是使用一个函数来调用另外一个函数。如下图,函数A将数据交给函数B处理,希望能的到结果返回,这里非常类型生产者和消费者,但是唯一缺少的就是我们上面所说的交易场所。正是缺少了这个交易场所,导致函数调用时出现一系列的问题,你经常发现只要函数B一旦修改就导致函数A中的调用也要修改,并且每当出现函数B奔溃时函数A也瞬间挂掉。这其实并不是最大的问题,如图,每当你调用函数B时,函数A就阻塞等待函数B,整个流程就变成了绝对的串行,函数B未执行完,函数A也将永远卡死在调用处。
那么解决方案其实就是需要把他们转化为一个生产消费者模型,为俩个函数之间也加上一个交易场所,这个交易场所可以存放函数A需要让函数B处理的数据,当交易场所存在数据时函数B就进行处理,否则函数A向交易场所添加数据,这个交易场所其实就是一段缓冲区,可以是链表,数组,队列任何结构,这样函数A和函数B的执行互不影响,并且大大增加了效率。
回到软件开发的本身归根结底来说,生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
我们生产者消费者模型使用最多的场景还是用于多线程中,在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
生产者消费者模型的优点
- 解耦:上面函数调用的例子其实就是为我们阐述传统函数调用强耦合的缺点,当有了此模型后购物例子中商家就不需要必须把商品送到消费者的手上,而是直接塞到超市中,这样厂家和消费者耦合度就大大减少了
- 支持并发:如果没有超市那个交易场所,每一次购物你都需要傻傻等待商家将商品送到你手上,或者是商家每家每户询问谁家需要商品,从线程的角度来说,他们是两个独立的并发主体,互不干扰的运行。
- 支持忙闲不均:支持忙闲不均,如果制造数据的速度时快时慢,缓冲区可以对其进行适当缓冲。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
角色间的关系
我们之前的博客讲了Linux下的同步与互斥,所以这里生产消费者模型也满足其中的关系。很明显生产者与生产者之间是互斥关系,因为他们之间存在一种竞争,他们都想自己生产物品供被人消费。而消费者与消费者也是互斥关系,很多同学可能会觉得这是不是出错了,消费者之间怎么能是互斥关系呢,那举个例子,超市现在只剩最后一包方便面了,然而你们都饿了,此时你们简直是不是需要竞争呢?最后生产者和消费者比较独特,他们既要互斥,还要同步,因为某一个时刻他们如果同时想生产或者消费就会产生互斥现象,而且从上述例子中我们可以得出,交易场所其实是共享资源,共享资源必须合理的让所有人都能访问,这也就是为什么需要同步。
这里为了便于记忆,你可以将上面这段话总结为123原则,每个生产消费者模型有1一个交易场所,2个角色,3种关系。相信现在你对生产消费者模型有了新的理解,那么我们接下来就来实现一个生产消费者模型。
基于阻塞队列的生产消费者模型
基于阻塞队列的生产者消费者模型显而易见,他最大的特点就在于他的交易场所使用了阻塞队列,对于阻塞队列你可能觉得陌生,不过你还记得Linux下进程间通信的匿名管道么?哎呦,是不是有感觉了,进程1就是生产者,进程2就是消费者,匿名管道实质上是一个文件,也是一个缓冲区,同样是我们的交易场所。我们这里实现的模型真的像极了进程间通信的管道。
这里我们实现了一个非常简单的阻塞队列,这里说明几点问题,因为选择了阻塞队列作为了交易场所,所以我们就需要队列的容器,但是原生的实现一个容器并不是一件容易的事,所以我们没有使用c语言,而是选择了c++,这样有的小伙伴看代码可能有的地方看不懂,不过笔者尽量写的非常偏c语言了,应该没什么大问题。
这里实现阻塞队列时为了简单起见,我们只考虑交易空的情况,当交易场所为空时通知生产者生产,所以我们就只需要一个条件变量即可,如果你想实现为空通知生产者生产,为满通知消费者消费那么你就需要再另外的增加一个条件变量来控制。
以下的代码因为没有规定队列的上限,所以为了避免生产者太快的情况我们让生产者生产一次就睡眠一秒,所以最后模型运行起来以后的现象是生产者生产一个数据,消费者就拿走一个数据。追加说明一点,因为这里的队列其实是共享资源,所以我们需要使用一把锁将他保护起来。
//queue.cc
#include<iostream>
#include<queue>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
using namespace std;
class BlockQueue{
private:
queue<int> q;
pthread_cond_t cond;
pthread_mutex_t lock;
private:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
bool isEmpty()
{
return q.size() == 0 ? true : false;
}
void ThreadWait()
{
pthread_cond_wait(&cond,&lock);
}
void WakeOneThread()
{
pthread_cond_signal(&cond);
}
public:
BlockQueue()
{
pthread_mutex_init(&lock,NULL);
pthread_cond_init(&cond,NULL);
}
void PushData(const int& data)
{
LockQueue();
q.push(data);
UnLockQueue();
cout << "product run done, data push sucess " << data <<endl;
WakeOneThread();
}
void PopData(int& data)
{
LockQueue();
while(isEmpty()){//使用while循环防止条件变量等待失败或者误唤醒
ThreadWait();
}
data = q.front();
q.pop();
UnLockQueue();
cout << "consume run done, data pop sucess : " << data <<endl;
}
~BlockQueue()
{
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&lock);
}
};
void* product(void* arg)//生产者生产
{
BlockQueue* bq = (BlockQueue*)arg;
srand((unsigned long)time(NULL));
while(1)
{
int data = rand()%100 + 1;
bq->PushData(data);
sleep(1);//防止生产太快,每生产一条睡眠一秒
}
}
void* consume(void* arg)//消费者消费
{
BlockQueue* bq = (BlockQueue*)arg;
while(1)
{
int d;
bq->PopData(d);
}
}
int main()
{
BlockQueue bq;
pthread_t p,c;
pthread_create(&p,NULL,product,(void*)&bq);
pthread_create(&c,NULL,consume,(void*)&bq);
pthread_join(p,NULL);
pthread_join(c,NULL);
}
代码执行结果:
仔细阅读上面的代码你一定能看的懂,如果有兴趣的同学还可以进一步修改代码,因为这里我们也并没有维护生产者生产者,消费者消费者之间的关系,但是想维护这俩个关系也很简单,你只需多创建几个线程,然后再创建俩把锁,再这些生产者生产者和消费者消费者竞争队列资源时,加上锁就可以。笔者这里仅为大家说明概念,篇幅太长就不在实现。
ps:不知道有没有小伙伴觉得从Linux下修改拷贝代码很麻烦,笔者偷偷告诉你个工具叫做gedit,这也是一个编辑器,你如果想拷贝很长的代码出来你用gedit + 代码文件打开文件,打开后直接用鼠标拷贝粘贴就好。昨天才无意中知道,真***的好用哈哈哈。
总结
到这里我们基于阻塞队列的生产消费者模型就介绍完了,但是小伙伴们不需要太关心代码的实现,你应该深刻理解这种模型的好处用途和思想,你要你记清楚笔者总结的那个123原则,相信能加深你对生产者消费者模型的记忆和理解,下篇博客笔者带领大家实现另一种更有意思的生产消费者模型。