目录
什么是生产者消费者模型?
生产者消费者模型是一种用于描述多线程编程中的协作关系的模型。该模型基于生产者和消费者的角色,生产者负责生产数据,消费者负责消费数据,在一个共享的缓冲区中协作完成数据传递。
在该模型中,多个生产者和多个消费者可能同时存在,对于生产者而言,如果缓冲区已经满了,就需要等待消费者来消费,直到缓冲区有空闲位置后再进行生产;对于消费者而言,如果缓冲区已经为空,就需要等待生产者来生产,直到缓冲区中有数据后再进行消费。
生产者消费者模型可以用于解决多线程编程中的同步和互斥问题,有效提高系统的并发性和效率。
为何要使用生产者消费者模型?
该模型就是通过一个容器来解决生产者和消费者的强耦合的问题,生产者和消费者彼此之间不直接通讯,而是通过生产者将生产的数据存放在阻塞队列中,而消费者从阻塞队列取数据,从而实现通讯。在这一过程中生产者生产完数据后不用等待消费者来取数据,而是直接将数据存放到阻塞队列中。消费者不用直接找生产者要数据,而是从阻塞队列中拿,阻塞队列就相当于一个数据缓冲区,平衡生产者和消费者对数据的处理能力。
生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:
三种关系:生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
两种关系:生产者和消费者(进程或线程)。
一个交易场所:通常指内存中的一段缓冲区。
以上我们可以称为321原则。要实现生产者消费者模型,本质就是对321原则进行维护。
生产者和生产者、消费者和消费者、生产者和消费者为什么会存在互斥关系?
介于生产者和消费者之间的交易容器会被多个生产者和消费者同时访问,因此我们需要将临界资源交易容器用互斥锁保护起来。这时生产者和生产者、消费者和消费者、生产者和消费者都会竞争式的去申请锁,所以说它们存在互斥关系。
生产者和消费者之间为什么会存在同步关系?
我们假想一下,如果生产者一直进行生产数据,将生产的数据放入交易场所的活动,当交易场所被数据塞满后,生产者还去生产数据就会导致生产失败。反之,如果消费者一直进行从交易场所中拿数据的消费活动,当交易场所中没有数据了,消费者的消费活动也就失败了。
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。
注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。
生产者消费者模型的优点:
1、解耦
2、支持并发
3、支持忙闲不均
如果在主函数中调用一个函数,主函数必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。
对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合。
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元 素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程 操作时会被阻塞)
为了方便理解,我们先来实现单生产单消费。
设计思路:
1、在单生产者、单消费者的生产者消费者模型中,不存在生产者和生产者之间的关系和消费者和消费者之间的关系,只需要维护生产者和消费者之间对队列访问同步与互斥关系即可。
2、将BlockingQueue当中存储的数据模板化,方便以后需要时进行复用。
3、将BlockingQueue存储数据的上限设置为5,阻塞队列中存储了五组数据时生产者就不能进行生产了,此时生产者就应该被阻塞。
4、阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来。
5、生产者线程要向阻塞队列当中Push数据,前提是阻塞队列里面有有可存放数据的空间,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
6、消费者线程要从阻塞队列当中Pop数据,前提是阻塞队列里面有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
7、因此在这里我们需要用到两个条件变量,一个条件变量用来描述队列为空,另一个条件变量用来描述队列已满。当阻塞队列满了的时候,要进行生产的生产者线程就应该在full条件变量下进行等待;当阻塞队列为空的时候,要进行消费的消费者线程就应该在empty条件变量下进行等待。
8、不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的,如果对应条件不满足,那么对应线程就会被挂起。但此时该线程是拿着锁的,为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
9、当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据,而此时可能有消费者线程正在empty条件变量下进行等待,因此当生产者生产完数据后需要唤醒在empty条件变量下等待的消费者线程。
10、同样的,当消费者消费完一个数据后,意味着阻塞队列当中至少有一个空间,而此时可能有生产者线程正在full条件变量下进行等待,因此当消费者消费完数据后需要唤醒在full条件变量下等待的生产者线程。
#include<iostream>
#include<pthread.h>
#include<queue>
#include<unistd.h>
using namespace std;
#define NUM 5
template<class T>
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_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);
}
private:
std::queue<T> _q;//阻塞队列
int _cap;//阻塞队列的最大容量
pthread_mutex_t _mutex;
pthread_cond_t _full;
pthread_cond_t _empty;
};
#include"blockqueue.hpp"
void* Producer(void* arg)
{
BlockQueue<int>*bd=(BlockQueue<int>*)arg;
while(1)
{
//sleep(1);
int data=rand()%100+1;
bd->Push(data);
cout<<"生产数据:"<<data<<endl;
}
}
void* Consumer(void*arg)
{
BlockQueue<int>*bd=(BlockQueue<int>*)arg;
while(1)
{
sleep(2);
int data=0;
bd->Pop(data);
cout<<"消费数据:"<<data<<endl;
}
}
int main()
{
srand((unsigned int)time(nullptr));
pthread_t producer,consumer;
BlockQueue<int>*bd=new BlockQueue<int>;
pthread_create(&producer,nullptr,Producer,bd);
pthread_create(&consumer,nullptr,Consumer,bd);
pthread_join(producer,nullptr);
pthread_join(consumer,nullptr);
delete bd;
return 0;
}
模拟实现计算任务
现在我们基于生产者消费者模型实现一个计算任务,负责生产计算任务的线程向阻塞队列push任务,而负责计算的线程则从阻塞队列中pop获取任务进行计算。
#include"blockqueue.hpp"
void* Producer(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
const char* arr = "+-*/%";
//生产者不断进行生产
while (true){
sleep(1);
int x = rand() % 100;
int y = rand() % 100;
char op = arr[rand() % 5];
Task t(x, y, op);
bq->Push(t); //生产数据
}
}
void* Consumer(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
//消费者不断进行消费
while (true){
sleep(1);
Task t;
bq->Pop(t); //消费数据
//处理数据
std::cout << "处理了一个任务:" <<std::endl;
t.Run();
}
}
int main()
{
srand((unsigned int)time(nullptr));
pthread_t producer,consumer;
BlockQueue<int>*bq=new BlockQueue<int>;
pthread_create(&producer,nullptr,Producer,bq);
pthread_create(&consumer,nullptr,Consumer,bq);
pthread_join(producer,nullptr);
pthread_join(consumer,nullptr);
delete bq;
return 0;
}
#include"blockqueue.hpp"
void* Producer(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
const char* arr = "+-*/%";
//生产者不断进行生产
while (true){
sleep(1);
int x = rand() % 100;
int y = rand() % 100;
char op = arr[rand() % 5];
Task t(x, y, op);
bq->Push(t); //生产数据
std::cout <<"生产了一个任务:"<< x << op << y << std::endl;
}
}
void* Consumer(void* arg)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
//消费者不断进行消费
sleep(2);
while (true){
Task t;
bq->Pop(t); //消费数据
//处理数据
std::cout << "处理了一个任务:";
t.Run();
}
}
int main()
{
srand((unsigned int)time(nullptr));
pthread_t producer,consumer;
BlockQueue<int>*bq=new BlockQueue<int>;
pthread_create(&producer,nullptr,Producer,bq);
pthread_create(&consumer,nullptr,Consumer,bq);
pthread_join(producer,nullptr);
pthread_join(consumer,nullptr);
delete bq;
return 0;
}