生产者和消费者模式是通过一个容器实现生产者和消费者的强耦合,彼此之间不通信,而是通过阻塞队列来通信,这个阻塞队列就是用来给生产者和消费者解耦的!
了解了模型初步概念,下面就来正式了解生产者和消费者模型的优点以及如何实现这个模型。
目录
一、生产者消费者模型优点
1、降低生产者和消费者的强耦合
阻塞队列可以看作是超市,有了超市,你每次只需要从超市里拿取物品,这样就无需要到深山老林的工厂里去拿取物品,这就解释了 为什么阻塞队列能让让生产者和消费者解耦。
2、支持并发
消费者在购买商品的同时,生产商也可以同时在生产商品。这只是一种策略。
支持并发并不代表必须要并发,你也可以先让消费者购买,等货架空了,再通知生产商来生产。
3、减少无用功
假设消费者的购买力很强,十分钟就把货架清空了,此时很显然就无法购买了,但是由于购买是一个循环的过程,即便货架空了,消费者也会来看看有没有到货,这很显然是无用功。这个时候阻塞队列的优势就体现出来了。
while(1)
{
if(goods>0)
{
goods--; //goods代表商品数量
}
}
当消费者的购买力很强时,超市的货架很快就被清空了,此时消费者就无法继续消费,此时应该让消费者等待(将消费者线程挂起),等生产商补充商品到一定程度的时候,再告诉消费者继续消费(唤醒消费者线程)。
当生产商的生产的速度很快时,超市的货架很快就会被填满了,此时生产商就不应该继续生产(生产商线程挂起),而是要等待消费者来购买,等减少到一定程度再继续生产(唤醒生产商线程)。
while(1)
{
if(goods>0)
{
goods--; //goods代表商品数量
}
else
{
pthread_cond_wait(...); //加入到条件变量中阻塞等待
}
}
二、生产者消费者模型代码实现
这个模型的核心是生产者输入商品的接口Push,消费者拿取的接口Pop,阻塞队列的基本容器使用的还是queue。下面我们一步步完成Push函数,熟悉了Push函数以后,Pop函数就很好理解了。完整的代码放在最后展示,中间会有一部分代码。
1、生产者向阻塞队列添加 — Push函数
(1) 添加一个容器来存放数据
阻塞队列的容器选用的是std中已有的queue,用于存放生产者向阻塞队列(超市)输送的数据
(2) 添加互斥锁
由于生产商线程和消费者线程都会访问这个阻塞队列,所以_container是一个临界资源,我们需要用锁来保护这个临界资源。要做的准备工作有创建锁 、构造函数中初始化锁、析构函数中销毁锁,然后我们就可以加锁和解锁了。
pthread_mutex_t _mtx; //创建互斥锁(保护临界资源)
(3) 添加条件变量
站在生产者的角度,当队列满了的时候,说明容器已经放不下了,要让消费者来消费了。此时要将当前线程加入到条件变量中阻塞等待,直到消费者线程向生产者条件变量发送信号,示意生产者可以继续添加内容了。
此时我们就需要一个条件变量(等待队列),条件变量和互斥锁一样,创建 + 初始化 + 销毁
pthread_cond_t _cond_productor; //生产者条件变量(等待队列)
(4) 实现Push函数
准备工作做好以后,我们就可以正式开始实现Push函数了,Push函数的核心是下面这句,然后我们在这个基础上添砖加瓦。
_container.push(val); //生产商向阻塞队列添加数据
第一步,加锁、解锁。_container是存放数据的容器,消费者线程和生产者线程都可以访问,属于临界资源,所以我们要加锁保护起来,在添加数据完毕以后解锁。
第二步,在添加数据之前,判断容器是否满了。如果满了,就不能让生产者继续添加数据了,所以要把生产者加入条件变量阻塞等待。
这里额外提一句,为什么要使用while,而不是if??上述我们所说的,是正常状态下,万一pthread_cond_wait函数调用失败呢??如果采用 if,生产者线程本该阻塞等待,却因为调用失败会继续向下运行,即继续向容器添加内容。
(5) 唤醒消费者条件变量中的线程
到了当前这一步生产者已经向队列里添加内容,所以我们可以发送信号告知消费者条件变量。我们也可以等到容器内的数据个数超过容器的一半时,再发送信号告知消费者条件变量解除挂起,让消费者线程来消费。这些策略可以自己决定。
pthread_cond_signal(&_cond_consumer); //每输入一个数据,就唤醒消费者线程来消费
下面是完整的Push函数
2、消费者从阻塞队列拿取 — Pop函数
现在我们可以照着Push函数的步骤实现Pop函数
(1) 添加条件变量
站在消费者的角度,当队列空了的时候,说明容器已经没有数据可取,要让生产者添加数据了。此时要将当前线程加入到条件变量中阻塞等待,直到消费者线程向生产者条件变量发送信号,示意生产者可以继续添加内容了。
pthread_cond_t _cond_consumer; //消费者条件变量(等待队列)
(2) 实现Pop函数
第一步,依然是加锁和解锁。
第二步,在删除数据之前,判断容器是否为空。如果是空的,消费者就不能继续消费了,此时要把消费者线程加入到对应的条件变量阻塞等待。
(3) 唤醒消费者条件变量中的线程
到了当前这一步,消费者已经从阻塞队列中拿走一个或者一部分数据了,我们可以等全部拿完再通知生产者条件变量继续生产,也可以等阻塞队列中数据个数减少到一半时再通知生产者条件变量。
pthread_cond_signal(&_cond_productor); //每拿走一个数据,就唤醒生产者线程来生产
下面是完整的Pop函数
3、代码测试
现在终于可以开始测试了,消费者线程暂时只创建一个,生产者线程也只创建一个,生产者输入的数据为随机数,暂定的策略为生产者生产一个,消费者就拿一个
如果你希望生产者生产的快一点,你可以缩短sleep的休眠时间或者不休眠
消费者也是同理
测试结果如下: