生产者消费者模型
生产者消费者模型的概念
生产者消费者模型的特点
生产者消费者模型优点
基于BlockingQueue的生产消费者模型
基于阻塞队列的生产者消费者模型
模拟实现基于阻塞队列的生产消费模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不进行直接通讯,而通过这个容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器中,消费者也不用找生产者要数据,而是直接从容器也就是阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的
如果缓冲区已经满了,则生产者线程阻塞;
如果缓冲区为空,那么消费者线程阻塞。
生产者消费者是多线程同步与互斥的一个经典场景,其特点如下:
生产者和生产者,消费者和消费者,生产者和消费者,它们之间为什么会存在互斥关系?
- 介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。
- 其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者,消费者和消费者,生产者和消费者之间都存在互斥关系。
生产者和消费者之间为什么会存在同步关系?
- 如果让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再生产数据就会生产失败。
- 反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。
注意:互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。
在多线程编程中,阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列的区别在于:
知识联系: 看到以上阻塞队列的描述,我们很容易想到的就是管道,而阻塞队列最典型的应用场景实际上就是管道的实现。
下面我们以单生产者、单消费者为例进行实现.
我们可以用C++STL库当中的queue实现阻塞队列
#include
#include
#include
template
#define NUM 5
class BlockQueue
{
private:
bool IsFull()//判满
{
return _q.size()==_cap;
}
bool IsEmpty()//判空
{
return _q.empty();
}
public:
BlockQueue(int cap=NUM)
:_cap(cap)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_full,nullptr);
pthread_cond_init(&_empty,nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destory(_full);
pthread_cond_destory(_empty);
}
//向阻塞队列插入数据(生产者调用)
void Push(const T& data)
{
pthread_mutex_lock(&_mutex);//加锁
while(IsFull())
{
//等待的,把线程挂起,我们当前是持有锁的
pthread_cond_wait(&_full,&_mutex);
}
_q.push(data);//向队列中放入数据,生产函数
pthread_cond_signal(&_empty);//唤醒在empty条件变量下等待的消费者线程
pthread_mutex_unlock(&_mutex);//解锁
}
//从阻塞队列中取数据
void Pop(T& data)
{
pthread_mutex_lock(&_mutex);//加锁
while(IsEmpty())
{
//不能进行消费,直到阻塞队列有新的数据
pthread_cond_wait(_empty,&_mutex);
}
data=_q.front();
_q.pop();
pthread_cond_signal(&_full);//唤醒在full条件变量下等待的生产者线程
pthread_mutex_unlock(&_mutex);//解锁
}
private:
std::queue _q;//阻塞队列
int _cap;//阻塞队列最大容器数据个数
pthread_mutex_t _mutex;//保护临界资源的锁
pthread_cond_t _full;//_q满的,消费者在该条件变量下等待
pthread_cond_t _empty;//_q空的,生产者在该条件变量下等待
};
相关说明:
注意:为什么判断是否满足生产消费条件时不能用if,而应该用while:
举个例子:比如现在我们准备进行生产数据(并且此时队列是满的),进入临界区后首先会进行判断:
if(IsFull()){
ProducterWait();
}假设在调用pthread_cond_wait函数时调用失败:也就是说此时阻塞队列是满的,但是该线程却没有成功进行等待,整个if语句结束后队列依旧是满的,于是执行完if语句接着往下执行时,进行了
bq_.push(in);
,造成的结果就是在队列满的情况下依旧进行了插入数据,明显这是不合理的。
为了避免出现上述情况,我们用while循环,这样如果挂起成功,在消费者通过条件变量告知生产者队列不为满的时候,就跳出循环;如果挂起失败,此时队列还是满的,那么条件满足,循环会继续下去,直到挂起成功,队列不为满,退出循环,进行生产者生产数据操作。
在主函数中我们就只需要创建一个生产者线程和一个消费者线程,让生产者线程不断生产数据,让消费者线程不断消费数据。
#include"BlockQueue.hpp"
#include
#include
#include
using namespace ns_blockqueue;
void *consumer(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
sleep(1);
int data = 0;
bq->Pop(&data);
std::cout << "消费者消费了一个数据: " << data << std::endl;
}
}
void *producter(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
sleep(1);
//1. 制造数据,生产者的数据(task)从哪里来??
int data = rand()%20 + 1;
std::cout << "生产者生产数据: " << data << std::endl;
bq->Push(data);
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue *bq = new BlockQueue();
pthread_t c,p;
pthread_create(&c, nullptr, consumer, (void*)bq);
pthread_create(&p, nullptr, producter, (void*)bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
相关说明:
小贴士: 以.hpp
为后缀的文件也是头文件,该头文件同时包含类的定义与实现,调用者只需include该hpp文件即可。因为开源项目一般不需要进行保护,所以在开源项目中用的比较多。
情况1:我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费(不让生产者进行sleep)。
void *producter(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
//sleep(1);
//1. 制造数据,生产者的数据(task)从哪里来??
int data = rand()%20 + 1;
std::cout << "生产者生产数据: " << data << std::endl;
bq->Push(data);
}
}
此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列塞满了,此时生产者想要再进行生产就只能进行等待,直到消费者消费完一个数据后,生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
情况2:我们也可以让生产者每隔一秒进行生产,而消费者不停的进行消费。
void *consumer(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
// sleep(1);
int data = 0;
bq->Pop(&data);
std::cout << "消费者消费了一个数据: " << data << std::endl;
}
}
这种情况我们会发现:虽然消费者消费的很快,但一开始阻塞队列中是没有数据的,因此消费者只能在empty条件变量下进行等待,直到生产者生产完一个数据后,消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调从一开始就是一致的。
情况3:在现实场景下,我们通常会在满足某一条件时再唤醒对应的生产者或消费者。
–》比如以超市为例:超市的货架就是我们这里的队列,一般超市的货物是不会有短缺的,因为通常假设进货了100个面包,店员一般会每天清点货物,每次他会在面包少于30个的时候就会通知厂家(生产者)进行补货,这样才会让超市的货物永远处于不断货的状态。
我们也可以通过一些方式,当阻塞队列当中存储的数据大于队列容量的一半时,再唤醒消费者线程进行消费;当阻塞队列当中存储的数据小于队列容器的一半时,再唤醒生产者线程进行生产。
void Push(const T &in)//插数据
{
LockQueue();
//临界区
while(IsFull()){ //bug?
//等待的,把线程挂起,我们当前是持有锁的!!!
ProducterWait();
}
//向队列中放数据,生产函数
bq_.push(in);
//控制消费条件
if(bq_.size() >= cap_/2 )
WakeupComsumer();
UnlockQueue();
}
void Pop(T *out)//取数据
{
LockQueue();
//从队列中拿数据,消费函数函数
while(IsEmpty()){ //bug?
//无法消费
ConsumerWait();
}
*out = bq_.front();
bq_.pop();
//控制生产条件
if(bq_.size() <= cap_/2 )
WakeupProducter();
UnlockQueue();
}
我们仍然让生产者生产的快,消费者消费的慢。运行代码后生产者还是一瞬间将阻塞队列塞满后进行等待,但此时不是消费者消费一个数据就唤醒生产者线程,而是当阻塞队列当中的数据小于等于队列容器的一半时,才会唤醒生产者线程进行生产。
基于计算任务的生产者消费者模型
例如,我们想要实现一个基于计算任务的生产者消费者模型,此时我们只需要定义一个Task类,这个类当中需要包含一个Run成员函数,该函数代表着我们想让消费者如何处理拿到的数据。
#pragma once
#include
#include
namespace ns_task
{
class Task
{
private:
int x_;
int y_;
char op_; //+/*/%
public:
// void (*callback)();
Task() {}
Task(int x, int y, char op) : x_(x), y_(y), op_(op)
{
}
int Run()
{
int res = 0;
switch (op_)
{
case '+':
res = x_ + y_;
break;
case '-':
res = x_ - y_;
break;
case '*':
res = x_ * y_;
break;
case '/':
res = x_ / y_;
break;
case '%':
res = x_ % y_;
break;
default:
std::cout << "bug??" << std::endl;
break;
}
std::cout << "当前任务正在被: " << pthread_self() << " 处理: " \
<< x_ << op_ << y_ << "=" << res << std::endl;
return res;
}
int operator()()
{
return Run();
}
~Task() {}
};
}
此时生产者放入阻塞队列的数据就是一个Task对象,而消费者从阻塞队列拿到Task对象后,就可以用该对象调用Run成员函数进行数据处理。
#include "BlockQueue.hpp"
#include "task.hpp"
using namespace ns_blockqueue;
using namespace ns_task;
void *consumer(void *args)
{
BlockQueue *bq = (BlockQueue *)args;//模板中放入的就是类型是Task
while (true)
{
// sleep(2);
// int data = 0;
// bq->Pop(&data);
// std::cout << "消费者消费了一个数据: " << data << std::endl;
Task t;
bq->Pop(&t); //完成任务消费的第一步
t(); //完成任务消费的第二步
}
}
void *producter(void *args)
{
BlockQueue *bq = (BlockQueue *)args;
string ops = "+-*/%";
while (true)
{
// sleep(1);
// 1. 制造数据,生产者的数据(task)从哪里来??
// int data = rand()%20 + 1;
// std::cout << "生产者生产数据: " << data << std::endl;
// bq->Push(data);
// 1. 制造数据
int x = rand() % 20 + 1;
int y = rand() % 10 + 1;
char op = ops[rand() % 5];
Task t(x, y, op);
cout << "生产者派发了一个任务:" << x << op << y << "=?" << endl;
// 2.将数据推送到任务队列中
bq->Push(t);
sleep(1);
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue *bq = new BlockQueue();
pthread_t c, p;
pthread_t c1, c2, c3, c4;
pthread_create(&c, nullptr, consumer, (void *)bq);
pthread_create(&c1, nullptr, consumer, (void *)bq);
pthread_create(&c2, nullptr, consumer, (void *)bq);
pthread_create(&c3, nullptr, consumer, (void *)bq);
pthread_create(&c4, nullptr, consumer, (void *)bq);
pthread_create(&p, nullptr, producter, (void *)bq);
pthread_join(c, nullptr);
pthread_join(c1, nullptr);
pthread_join(c2, nullptr);
pthread_join(c3, nullptr);
pthread_join(c4, nullptr);
pthread_join(p, nullptr);
return 0;
}
我们让生产者每隔一秒进行输入任务,此时消费者进行计算任务。我们也可以定义多个消费者,让不同的消费者进行计算任务。