1.生产者消费者模型是什么
听到生产者-消费者模型,大家是不是觉得有点熟悉,好像在那年高中生物课上听过这两个概念,其实这里要讲的生产消费模型中的组成部分和它差不多,即由生产者、消费者、交易场所三部分构成。类比下面商场关系图就能一目了然~
而linux中的生产者消费者模型具体来说,就是在一个系统中,存在生产者和消费者两种角色,他们通过内存缓冲区进行通信,生产者生产消费者需要的数据,消费者使用这些数据做成其他产品。
例如用户注册的过程主要有以下几个步骤,
1)用户在前台输入用户名&密码&验证码
2)数据被分解并发送给后台各个服务(验证码验真和用户信息存储分成两个独立服务)
3)后台服务有自己的内存缓冲区即请求队列进行数据暂存
4)然后各服务从缓存区取数据进行相应处理
其中数据分发的角色相当于生产者提供数据,请求队列相当于交易场所存储数据,不同服务即相当于消费者使用数据。
模型遵循的原则:321原则
三种关系 | 生产者与生产者之间互斥关系,消费者与消费者之间互斥关系,生产者与消费者之间同步关系 |
---|---|
两种角色 | 生产者,消费者 |
一个交易场所 | 队列,链表,数组等任何一个可以作为缓冲区的结构或者能够提供通信的方式 |
2.生产者-消费者模式
了解了生产者消费者模型,不难理解生产者消费者模式就是生产者与消费者不需要进行直接通信,通过缓冲区-阻塞队列来辅助两者进行通讯。
在系统里的体现其实就是通过容器解决生产者和消费者之间的强耦合问题,同时解决了生产消费能力不均衡的问题,即生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不用找生产者要数据只要从阻塞队列里取就行,这时阻塞队列就相当于一个缓冲区,即解决了生产者与消费者间的强耦合问题,又平衡了生产者和消费者的处理能力。
由此看这个第三者-阻塞队列的作用非常大。事实上,大多数的设计模式都有类似的第三者,例如 如工厂模式的第三者是工厂类,模板模式的第三者是模板类,在学习时我们只要找到这个第三者就能快速熟悉一个设计模式。
生产消费模式的优点
- 解耦:
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者类中的代码发生变化,可能会影响到生产者必须做出相应的改变。
使用生产者/消费者模式后,两者之间不需要直接依赖,两者各自依赖的是指定的某个缓冲区,因此耦合也就相应降低了。
举个例子:若没有超市,那么生产商品的厂家需要直接把商品递到消费者手中,如果消费者发生变化,那么生产厂家必须重新维护消费者耗时耗力,即对厂家的影响非常大(强耦合)。
但用了此模型后,厂家生产的商品放在超市就好了,消费者无论怎么变它都不用关心,只需要保证商品到达超市就可以了,这样能使厂家和消费者耦合度就大大减少了
- 支持并发:
让生产者直接调用消费者的某个方法还有一个弊端,就是函数调用是同步的(或者说阻塞的),在未拿到消费者的返回值之前,生产者必须等待。若消费者处理数据非常慢,那么生产者就无法提高效率(反之亦然)。
使用生产者/消费者模式后,生产者和消费者可以是两个独立的并发主体,这时生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据,基本上不用依赖消费者的处理速度,因此能够通过并发大大提高程序处理效率。
举个例子:若没有超市,每一次购物消费者都需要一直等待商家,直到商家将商品送到手上,或者是商家每家每户询问谁家需要商品,效率非常慢。
有了此模型后,从线程的角度来说,商家和消费者是两个独立的主体,他们可以互不干扰的运行。甚至支持消费者同时购买多个商品,商家同时供货多个超市。
- 支持忙闲不均:
如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区。等生产者的制造速度慢下来,消费者再慢慢处理掉。
还是超市的例子:若没有超市,且商家供货速度非常快,那么消费者就需要快速的处理掉商品,那消费者的拿不了这些商品的时候,就会迫使商家降低生产速度,如此就浪费了生产力。
有了该模型后,消费者无需一次性拿完生产者提供的商品,生产者只管生产,消费者可以通过多次去超市完成拿这些商品的工作。
3.基于阻塞队列的生产消费模式
在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型的数据结构。这里的阻塞队列与进程间通信中的管道作用相似,并且都自带同步互斥机制。其与普通队列的区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入元素;当队列满时,往队列里放元素的操作被阻塞,直到队列中有元素被取出。
模拟基于阻塞队列的生产者-消费者模式
这里需要注意以下的代码因为没有规定队列的上限,所以为了避免生产者太快的情况我们让生产者生产一次就睡眠一秒,所以最后模型运行起来以后的现象是生产者生产一个数据,消费者就拿走一个数据。追加说明一点,因为这里的队列其实是共享资源,所以我们需要使用一把锁将他保护起来。
代码如下:
#include <iostream>
#include <queue>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.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 WakeConsume()
{
pthread_cond_signal(&cond);
}
void ProductWait()
{
pthread_cond_wait(&cond,&lock);
}
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;
WakeConsume();
}
void PopData(int &data) //消费者消费,无法消费时等待
{
LockQueue();
while(isEmpty()) //用while可防止误唤醒同时等待条件满足
ProductWait();
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);
return 0;
}
运行结果:
上面的代码你一定没问题,如果有兴趣的同学还可以进一步修改代码,因为这里我们也并没有维护生产者生产者,消费者消费者之间的关系,但是想维护这俩个关系也很简单,你只需多创建几个线程,然后再创建俩把锁,再这些生产者生产者和消费者消费者竞争队列资源时,加上锁就可以。我就不在这里实现了,篇幅有点长~
如何选择不同的结构实现生产者-消费者模式
若数据流量不是很大可直接用阻塞队列、考虑内存分配性能用环形缓冲区(固定大小后无需频繁对缓冲区进行分配、释放),考虑同步&互斥性能可用双缓冲区(避免共用一个队列时效率低下甚至造成死锁,减少用户态/核心态切换)。
下一篇会讲到基于环形队列的生产消费模型,有兴趣的记得戳链接哦~