什么是生产者消费者模型?
生产者消费者模型是操作系统中一种重要的模型,它描述的是一种等待和通知的机制
一. 引子
生活中就有很多生产者消费者模型,比如购物
我们会取超市买一些生活用品,显而易见,我们就是消费者。那超市是生产者吗?其实不是,超市只是一个交易场所,真正的生产者是工厂,也就是供应商。
而将这种关系应用到多线程中,生产者就是承担生产数据的线程,消费者就是接收数据的线程,而交易场所是一种特殊的缓冲区
而整个过程也可以类比成送快递
1.商家将商品包装好 —— 相当于生产者生产数据
2.商家将快递放到快递站 —— 相当于生产者将数据放入缓冲区
3.快递员将商品从快递站取出 —— 相当于消费者将数据从缓冲区取出
4.快递员将商品送出 —— 相当于消费者对数据做处理
二. 生产者消费者模型的作用
其实,生产者消费者模型通过交易场所,也就是缓冲区,完成了生产者和消费者之间的解耦,生产者和消费者没有直接交互,统一通过和缓冲区交互,来达到数据传递的效果。
同时,缓冲区还解决了,多个消费者,生产者要将数据传递给谁;多个生产者,消费者要从谁那拿数据。但生产和消费不同步时,缓冲区也可以完成任务。生产者多生产的数据,存放到缓冲区中,方便在不生产时,消费者仍有数据可读;再消费者不读取数据时,缓冲区满时,提醒生产者不需要再生产了。
这就是生产者消费者模型的好处。
三. 生产者/消费者模型的记忆原则
生产者消费者模型中有三种关系:
生产者—生产者
消费者—消费者
生产者—消费者
而三者之间的关系如下:
- 生产者—生产者:
互斥
因为缓冲区大小有限,所以放数据时,生产者之间需要互斥 - 消费者—消费者:
互斥
对于同一种资源(数据),消费者之间存在竞争关系,所以需要互斥 - 生产者—消费者:
既同步,又互斥
在生产者放入数据和消费者取出数据时,都不允许对方对缓冲区进行操作,避免受到影响,所以需要互斥。另外缓冲区没有数据时,消费者不能读数据;缓冲区数据满时,生产者不能放数据,所以生产者和消费者需要按照一定顺序访问缓冲区,所以二者之间还是同步
四. 简单的生产者消费者模型
生产者消费者模型就两个关系
:生产者和消费者
还有一个场景
:交易场所(缓冲区)
而通过交易场所的不同,我们可以实现 不同的生产者消费者模型
1. 阻塞队列
阻塞队列是实现生产者消费者模型的一种缓冲区
BlockQueue,阻塞队列,是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取数据的操作将被阻塞,直到队列被放入数据;当队列满时,往队列里存放数据的操作也会被阻塞,直到有元素被从队列中取出(以上操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
图中Thread 1是生产者,Thread 2是消费者。
接下来,我们简单使用阻塞队列实现生产者消费者模型
条件变量相关使用可以参看【Linux】线程同步&条件变量
互斥量相关使用可以参看【Linux】线程互斥
程序的目的是:生产者通过随机数种子生成数字,再通过阻塞队列,传送给消费者。消费者通过阻塞队列获取数据,然后输出。
首先,我们先实现阻塞队列
因为生产者和消费者之间是同步并互斥的关系,所以在生产者放数据,消费者取数据时,都应该保持互斥,并且根据阻塞队列的容量大小,按一定顺序执行动作
为此,我们需要在阻塞队列中同时封装互斥锁和条件变量
#include<iostream>
#include<queue>
#include<pthread.h>
//默认的队列的大小
int gcap=5;
template<class T>//模板
class blockQueue
{
private:
std::queue<T> _q;//存放资源的队列
int _cap;//队列的容量
pthread_mutex_t _mutex;//互斥锁
pthread_cond_t _consumerCond;//消费者的条件变量,当队列空时,wait
pthread_cond_t _producerCond;//生产者的条件变量,当队列满时,wait
};
因为生产者和消费者等待的条件不一样,所以需要两个条件变量,但是二者访问同一临界区,所以只需要一个互斥锁
在阻塞队列初始化时,我们需要对互斥锁和条件变量做初始化,在阻塞队列销毁时,也同样需要对其进行销毁。
//构造函数
blockQueue(int cap=gcap)
:_cap(cap)
{
//初始化锁和条件变量
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_consumerCond,nullptr);
pthread_cond_init(&_producerCond,nullptr);
}
//析构函数
~blockQueue()
{
//销毁互斥锁和条件变量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_producerCond);
}
接下来是生产者放数据的函数
访问临界区前一定需要加锁,判断条件也是在访问临界资源,所以也需要在加锁后。
生产者需要先判断当前阻塞队列数据是否已满,如果满了,则需要在_producerCond条件变量上等待,而如果等待被唤醒,则会继续从wait处开始运行,放入数据,同时因为有数据产生,可以唤醒等待的消费者
//判断队列是否满
bool isFull(){ return _cap==_q.size(); }
//放数据
void push(T&in)
{
//访问临界区
//加锁
pthread_mutex_lock(&_mutex);
//判断队列是否满
while(isFull())
{
//队列为满说明不需要再生产,开始等待
pthread_cond_wait(&_producerCond,&_mutex);
}
//放入数据
_q.push(in);
//有生产数据就可以唤醒消费者
pthread_cond_signal(&_consumerCond);
//解锁
pthread_mutex_unlock(&_mutex);
}
消费者取数据
//判断队列是否为空
bool isEmpty(){ return _q.empty(); }
//取数据
void pop(T*out)
{
//访问临界区加锁
pthread_mutex_lock(&_mutex);
//判断队列是否为空
while(isEmpty())
{
//为空则不需要再拿数据
//在消费者条件变量上等待
pthread_cond_wait(&_consumerCond,&_mutex);
}
//取出数据
*out=_q.front();
_q.pop();
//唤醒生产者
pthread_cond_signal(&_producerCond);
//访问结束,解锁
pthread_mutex_unlock(&_mutex);
}
消费者取数据的基本思路同生产者放数据,不过消费者需要判断的条件是当前阻塞队列是否为空,同时out作为输出型参数,需要返回取出的数据,然后因为取出数据,有空位可以生产,所以唤醒等待的生产者。
以上就是简单的阻塞队列的实现方式,完整代码如下:
blockQueue.hpp
#include<iostream>
#include<queue>
#include<pthread.h>
//默认的队列的大小
int gcap=5;
template<class T>
class blockQueue
{
public:
//构造函数
blockQueue(int cap=gcap)
:_cap(cap)
{
//初始化锁和条件变量
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_consumerCond,nullptr);
pthread_cond_init(&_producerCond,nullptr);
}
//析构函数
~blockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_producerCond);
}
//判断队列是否满
bool isFull(){ return _cap==_q.size(); }
//判断队列是否为空
bool isEmpty(){ return _q.empty(); }
//放数据
void push(T&in)
{
//访问临界区
//加锁
pthread_mutex_lock(&_mutex);
//判断队列是否满
while(isFull())
{
//队列为满说明不需要再生产,开始等待
pthread_cond_wait(&_producerCond,&_mutex);
}
//放入数据
_q.push(in);
//有生产数据就可以唤醒消费者
pthread_cond_signal(&_consumerCond);
//解锁
pthread_mutex_unlock(&_mutex);
}
//取数据
void pop(T*out)
{
//访问临界区加锁
pthread_mutex_lock(&_mutex);
//判断队列是否为空
while(isEmpty())
{
//为空则不需要再拿数据
//在消费者条件变量上等待
pthread_cond_wait(&_consumerCond,&_mutex);
}
//取出数据
*out=_q.front();
_q.pop();
//唤醒生产者
pthread_cond_signal(&_producerCond);
pthread_mutex_unlock(&_mutex);
//访问结束,解锁
pthread_mutex_unlock(&_mutex);
}
private:
std::queue<T> _q;//存放资源的队列
int _cap;//队列的容量
pthread_mutex_t _mutex;//互斥锁
pthread_cond_t _consumerCond;//消费者的条件变量,当队列空时,wait
pthread_cond_t _producerCond;//生产者的条件变量,当队列满时,wait
};
简单使用一下
main.cc
#include "blockQueue.hpp"
#include <ctime>
#include <unistd.h>
using namespace std;
// 消费者生产者启动函数
void *consumer(void *args)
{
blockQueue<int> *bq = static_cast<blockQueue<int> *>(args);
int data=0;
while(true)
{
sleep(1);
bq->pop(&data);
cout<<"消费者获取数据: "<<data<<endl;
}
}
// 生产者启动函数
void *producer(void *args)
{
blockQueue<int> *bq = static_cast<blockQueue<int> *>(args);
while (true)
{
sleep(1);
// 生产1~10的数字
int val = rand() % 10 + 1;
cout<<"生产者生产数据: "<<val<<endl;
bq->push(val);
}
}
int main()
{
blockQueue<int> *bq=new blockQueue<int>();
// 随机数种子
srand((uint64_t)time(nullptr));
pthread_t Consumer;
pthread_t Producer;
//创建线程
pthread_create(&Consumer, nullptr, consumer, bq);
pthread_create(&Producer, nullptr, producer, bq);
//线程等待
pthread_join(Consumer,nullptr);
pthread_join(Producer,nullptr);
delete dp;
return 0;
}
运行结果如下:
2. 环形队列
环形队列采用数组模拟,用模运算来模拟环状特性。
使用信号量
控制生产者和消费者可访问的空间数量
首先设计环形队列:环形队列就是交易场所。
不同于阻塞队列,生产者和消费者访问的大概率是不同的位置,将环形队列不看作整体,而是根据容量分散成每一部分,并使用两个信号量控制——空间信号量
(生产者关心)和资源信号量
(生产者关心)
基本框架如下:
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<mutex>
//环形队列的默认大小
static const int N=5;
template<class T>
class RingQueue
{
public:
RingQueue(int num=N):_ring(num),_cap(num)
{
//第一个0表示线程间共享,第二个0代表初始值为0
sem_init(&_data_sem,0,0);
sem_init(&_space_sem,0,num);
//互斥锁初始化
pthread_mutex_init(&_c_mutex,nullptr);
pthread_mutex_init(&_p_mutex,nullptr);
_c_pos=_p_pos=0;//环形队列访问位置初始化
}
~RingQueue()
{
//信号量的销毁
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);
//互斥锁的销毁
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
std::vector<T>_ring;//环形队列
int _cap;//容量
sem_t _data_sem;//资源信号量,消费者关心
sem_t _space_sem;//空间信号量,生产者关心
int _c_pos;//消费者访问位置
int _p_pos;//生产者访问位置
pthread_mutex_t _c_mutex;//消费者互斥锁
pthread_mutex_t _p_mutex;//生产者互斥锁
};
然后是放资源
和拿资源
的逻辑:
//申请信号量
void P(sem_t &s)
{
sem_wait(&s);
}
//归还信号量
void V(sem_t &s)
{
sem_post(&s);
}
//生产者
void push(const T&in)
{
P(_space_sem);//空间信号量-1
pthread_mutex_lock(&_p_mutex);
_ring[_p_pos++]=in;
_p_pos%=_cap;
pthread_mutex_unlock(&_p_mutex);
V(_data_sem);//资源信号量+1
}
//消费者
void pop(T&out)
{
P(_data_sem);//资源信号量-1
pthread_mutex_lock(&_c_mutex);
out=_ring[_c_pos++];//取出资源
_c_pos%=_cap;
pthread_mutex_unlock(&_c_mutex);
V(_space_sem);//空间信号量+1
}
完整代码如下:
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<mutex>
//环形队列的默认大小
static const int N=5;
template<class T>
class RingQueue
{
private:
//申请信号量
void P(sem_t &s)
{
sem_wait(&s);
}
//归还信号量
void V(sem_t &s)
{
sem_post(&s);
}
public:
RingQueue(int num=N):_ring(num),_cap(num)
{
//第一个0表示线程间共享,第二个0代表初始值为0
sem_init(&_data_sem,0,0);
sem_init(&_space_sem,0,num);
//互斥锁初始化
pthread_mutex_init(&_c_mutex,nullptr);
pthread_mutex_init(&_p_mutex,nullptr);
_c_pos=_p_pos=0;//环形队列访问位置初始化
}
//生产者
void push(const T&in)
{
P(_space_sem);//空间信号量-1
pthread_mutex_lock(&_p_mutex);
_ring[_p_pos++]=in;
_p_pos%=_cap;
pthread_mutex_unlock(&_p_mutex);
V(_data_sem);//资源信号量+1
}
//消费者
void pop(T&out)
{
P(_data_sem);//资源信号量-1
pthread_mutex_lock(&_c_mutex);
out=_ring[_c_pos++];//取出资源
_c_pos%=_cap;
pthread_mutex_unlock(&_c_mutex);
V(_space_sem);//空间信号量+1
}
~RingQueue()
{
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
std::vector<T>_ring;//环形队列
int _cap;//容量
sem_t _data_sem;//资源信号量,消费者关心
sem_t _space_sem;//空间信号量,生产者关心
int _c_pos;//消费者访问位置
int _p_pos;//生产者访问位置
pthread_mutex_t _c_mutex;//消费者互斥锁
pthread_mutex_t _p_mutex;//生产者互斥锁
};
结束语
感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。