目录
生产者消费者模型
生产者消费者模型概念
生产者消费者模型特点
生产者消费者模型优点
基于BlockingQueue的生产者消费者模型
BlockingQueue
C++ queue模拟阻塞队列的生产消费模型
如何使用该阻塞队列
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题
以日常买东西为例:我们去超市买商品时并不需要关注商品厂商有没有生产,商品厂商开工生产时也并不关注现在有没有消费者需要购买商品。厂商将生产的商品交给超市,消费者有需要再从超市中挑选购买,这就给生产者和消费者进行了解耦。
生产者消费者模式就是通过生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型是我们解决多线程问题经常使用的一个模型,其特点简称321原则:
三种关系:生产者和生产者(互斥)、消费者和消费者(互斥)、生产者和消费者(同步、互斥)
两种角色:生产者(一个或多个线程)和消费者(一个或多个线程)
一个交易场所:自己组织的一段缓冲区(如阻塞队列等)
为什么生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系?
总结:介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源保护起来。因此,生产者和消费者都会竞争式的申请锁,所以生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。
为什么生产者和消费者之间会存在同步关系?
因此我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。
注意:
互斥关系:保证数据的正确性。
同步关系:为了让多线程之间协同起来。
解耦
支持并发
支持忙闲不均
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
知识迁移:阻塞队列最典型的应用场景实际上就是管道的实现。
为了便于理解,我们以单生产者,单消费者,来进行讲解:
#pragma once
#include
#include
#include
namespace sola_blockqueue
{
template
class BlockQueue
{
public:
BlockQueue(const int cap = 5)
:_cap(cap)
{
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&in ) //生产
{
LockQueue();
while(IsFull())
{
ProducterWait();
}
bq.push(in);
if(bq.size()>_cap/2)
ConsumerBroadcast();
//可以设计策略,如放的量过了一半再唤醒消费者
UnLockQueue();
}
void Pop(T* out) //消费 输出型参数用指针 输入输出用& 输入const &
{
LockQueue();
while(IsEmpty())
{
ConsumerWait();
}
*out = bq.front();
bq.pop();
//可以设计策略,剩余量小于一半时再唤醒生产者
if(bq.size()<_cap/2)
ProducterBroadcast();
UnLockQueue();
}
private:
bool IsFull() //判断阻塞队列是否已满
{
return bq.size()==_cap;
}
bool IsEmpty() //判空
{
return bq.size()== 0;
}
void LockQueue() //申请互斥量
{
pthread_mutex_lock(&mutex);
}
void UnLockQueue() //解锁
{
pthread_mutex_unlock(&mutex);
}
void ProducterWait() //阻塞队列已满,让生产者挂起等待
{
//1.调用时,自动释放mutex,再挂起
//2.返回时,首先申请mutex,拿到锁后才能返回。
pthread_cond_wait(&full,&mutex);
}
void ConsumerWait() //阻塞队列为空,让消费者挂起等待
{
pthread_cond_wait(&empty,&mutex);
}
void ProducterSignal() //消费者消费后唤醒生产者生产
{
pthread_cond_signal(&full);
}
void ConsumerSinal() //生产者生产后,唤醒消费者消费
{
pthread_cond_signal(&empty);
}
void ProducterBroadcast()
{
pthread_cond_broadcast(&full);
}
void ConsumerBroadcast()
{
pthread_cond_broadcast(&empty);
}
private:
std::queue bq; //我们的阻塞队列
int _cap; //容量上线
pthread_mutex_t mutex; //保护临界资源的锁
//生产满了的时候就停止生产,让消费者来消费 消费空了,就生产
pthread_cond_t full; //bq满了,生产者等待
pthread_cond_t empty; //bq空了 消费者等待
};
细节说明:
由于我们实现的是单生产者、单消费者的生产者消费者模型,因此我们只需要维护生产者和消费者之间的同步与互斥关系即可。
使用模板方便我们接收各种数据,因为队列中存储的还可能是方法。
默认构造给的缺省值是5,阻塞队列中最多存放五份数据。
一个平时写代码的规范:输出型参数用指针,输入输出型参数用引用,输入型参数用const引用。
阻塞队列是临界资源,我们需要一把锁将其保护起来。
我们需要用到两个条件变量,full和empty,分别用来描述阻塞队列已满和阻塞队列为空的情况。
生产者线程要向阻塞队列当中Push数据,若阻塞队列已满,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
消费者线程要从阻塞队列当中Pop数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
pthread_cond_wait函数的特点:
调用时,自动释放mutex,再挂起。
返回时,首先申请mutex,拿到锁后,才能返回。
阻塞队列为空时,生产者生产后就可以唤醒消费者,此时阻塞队列不为空;阻塞队列已满时,消费者消费后可唤醒生产者此时阻塞队列不为满。
判断是否满足生产消费条件时不能用if,而应该用while:
pthread_cond_wait函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。
其次,在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时就会出现问题。
使用while进行重复的判断就可以解决上述问题,当条件满足时方可向下执行。
存入和取出数据
#include "BlockQueue.hpp"
#include
#include
#include
#include
using namespace sola_blockqueue;
void* Consumer(void* _bq) //消费者
{
BlockQueue* bq = reinterpret_cast*>(_bq);
//不断消费
while(true)
{
int data;
bq->Pop(&data);
std::cout<<"消费者消费一个数据:"<* bq = reinterpret_cast*>(_bq);
//不断生产
while(true)
{
sleep(1);
//制造资源
//生产者数据(task)从哪来
int data = rand()%20 + 1 ;
std::cout<<"生产者生产数据:"<Push(data);
}
return nullptr;
}
int main()
{
srand(time(nullptr)^getpid());
pthread_t tid_1,tid_2;
BlockQueue* bq = new BlockQueue;
pthread_create(&tid_1,nullptr,Consumer,reinterpret_cast(bq));
pthread_create(&tid_2,nullptr,Producter,reinterpret_cast(bq));
pthread_join(tid_1,nullptr);
pthread_join(tid_2,nullptr);
return 0;
}
生产者,先生产数据,当数据超过阻塞队列的一半时再唤醒消费者进行消费。
存入和取出任务
为此我们先写一个Task方法:
#pragma once
#include
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+')
: a(x), b(y), _op(op)
{
}
~Task()
{
}
void Run()
{
int ret = 0;
switch (_op)
{
case ('+'):
ret = a+b;
break;
case ('-'):
ret = a-b;
break;
case ('*'):
ret = a*b;
break;
case ('/'):
ret = a/b;//暂不考虑b为0;
break;
case ('%'):
ret = a%b;
break;
default:
std::cout << "未知操作符" << std::endl;
break;
}
std::cout<< a << _op << b << '=' << ret << std::endl;
}
void operator()()
{
Run();
}
private:
int a;
int b;
char _op;
};
该方法用于进行两个操作数之间的简单运算。
#include "BlockQueue.hpp"
#include
#include
#include
#include
#include "Task.hpp"
using namespace sola_blockqueue;
void* Consumer(void* _bq)
{
BlockQueue* bq = reinterpret_cast*>(_bq);
//不断消费
while(true)
{
Task t;
bq->Pop(&t);
std::cout<<"消费者:";
t();
}
return nullptr;
}
void* Producter(void* _bq)
{
BlockQueue* bq = reinterpret_cast*>(_bq);
//不断生产
while(true)
{
sleep(1);
//制造资源
//生产者数据(task)从哪来
int x = rand()%20 +1;
int y = rand()%10 +1;
char op = "+-*/%"[rand()%5]; //随机取一个操作符
Task t(x,y,op);
std::cout<<"生产者生产了一个方法"<Push(t);
}
return nullptr;
}
int main()
{
srand(time(nullptr)^getpid());
pthread_t tid_1,tid_2;
BlockQueue* bq = new BlockQueue;
pthread_create(&tid_1,nullptr,Consumer,reinterpret_cast(bq));
pthread_create(&tid_2,nullptr,Producter,reinterpret_cast(bq));
pthread_join(tid_1,nullptr);
pthread_join(tid_2,nullptr);
return 0;
}
我们实现了生产者生产一批方法,然后消费者进行执行的效果。