生产者消费者模型
什么是生产者消费者模型?
生产者消费者模式就是通过一个容器
来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
对于厂商,当厂商生成完商品后,并不会直接拿给消费者,而是将商品交付给超市,消费者去超市买商品;
对于消费者,不会直接去厂商买商品,而是去超市买;
这样生产者和消费者两个对象就解耦合了
生产者消费者模型的特点
321原则(便于记忆)
- 三种关系: 生产者和生产者(互斥)、消费者和消费者(互斥)、生产者和消费者(互斥、同步)
- 两种角色: 生产者和消费者(进程或线程)
- 一个交易场所: 通常指的是内存中的一段缓冲区
- 产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来
- 生产者消费者模型还支持多个线程一起进入,阻塞队列属于临界资源,所以同一时刻,不管是生产者还是消费者,都只允许一个线程进入到阻塞队列进行push或pop操作
生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?
于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。
其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。
生产者和消费者之间为什么会存在同步关系?
如果让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再生产数据就会生产失败
反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败
互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来
生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
如果我们在主函数中调用某一函数,那么我们必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合
在生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合
基于BlockingQueue的生产者消费者模型
与普通的队列的区别:
当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素
当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
#define NUM 5
template<class T>
class BlockQueue
{
public:
BlockQueue(int capacity= NUM)
: _capacity(capacity)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_full, nullptr);
pthread_cond_init(&_empty, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full);
pthread_cond_destroy(&_empty);
}
//向阻塞队列插入数据(生产者调用)
void Push(const T& data)
{
pthread_mutex_lock(&_mutex);
while (IsFull())
{
//不能进行生产,直到阻塞队列可以容纳新的数据
pthread_cond_wait(&_full, &_mutex);
}
_q.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
}
// 从阻塞队列获取数据(消费者调用)
void Pop(T& data)
{
pthread_mutex_lock(&_mutex);
while (IsEmpty())
{
// 不能进行消费,直到阻塞队列有新的数据
pthread_cond_wait(&_empty, &_mutex);
}
data = _q.front();
_q.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_full); // 唤醒在full条件变量下等待的生产者线程
}
bool IsFull()
{
return _q.size() == _capacity;
}
bool IsEmpty()
{
return _q.empty();
}
private:
std::queue<T> _q; // 阻塞队列
int _capacity; // 阻塞队列最大容器数据个数
pthread_mutex_t _mutex;
// 通过条件变量来判断队列信息
pthread_cond_t _full; // 表示阻塞队列是否为满的条件
pthread_cond_t _empty; // 表示阻塞队列是否为空的条件
};
- 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素;
- 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出;
- 一个条件变量用来描述队列为空,另一个条件变量用来描述队列已满;
- 阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来;
判断是否满足生产消费条件时不能用if,而应该用while
pthread_cond_wait
函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行
在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast
函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了
为了确认是否真的满足生产消费条件,这里必须使用while进行判断