生产者与消费者模型
产生原因: 生产者生产的数据过快,消费者处理数据,但是生产者和消费者的速度并不均衡,导致生产的速度或者数据处理的速度根本提不起来。举个栗子:一个线程从网卡上抓取数据流量,一个线程进行流量分析,分析肯定特别慢,如果你从网卡上抓取的数据流量很慢的话,不代表流量产生的就慢,抓的特别慢就说明丢包了,意味着你抓取流量数据的时候抓取的不完整,不完整说明你的数据分析没有任何意义。你只能将数据丢掉,那么就遗失数据。
为什么要使用生产者消费者模型??
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取数据,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
关于耦合性的解释:
耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。
生产者与消费者模型的介绍:
生产者与消费者模型:(不管什么模型都是针对于特定场景下的解决方案)
典型场景: 任务处理中既有数据生产,又有数据处理的这种场景
三个重要成员:
- 生产者: 生产数据
- 缓冲区(队列):缓存数据,且线程安全(自带同步与互斥,有数据就取,没有就等待产生)
- 消费者:消费数据
生产者消费者模型的优点:
- 支持忙闲不均。 生产者生产的快,可以存储到缓冲区队列中,等待多启动几个线程一起去消费数据,利用数据。同理,当生产者生产的数据慢,缓冲区队列中么有数据,那么消费者线程则阻塞等待有数据的产生。
- 支持并发。 有多个生产者或者多个消费者的时候,处理速度能达到非常快。毕竟中间的缓冲区时线程安全的。
- 解耦合。封装就是一种解耦合,减小关联性,修改只修改一个,不会对另一个造成影响,生产者如果直接将数据交给消费者的话,之间的关联性很大,而现在有了队列,他们只与队列打交道,并不直接沟通,数据处理与数据生产直接没有直接关系。
生产者与消费者模型可以总结为: 一个场所,两类角色,三种关系
一个场所:在缓冲区队列中
两类角色:消费者与生产者线程
三种关系:
1. 生产者与消费者: 具有同步、互斥关系(放完数据,才可以去取数据)
2. 生产者与生产者: 互斥关系(特定的数据只能一个线程生产)
3. 消费者与消费者: 互斥关系(一个资源只能一个线程去取)
生产者与消费者模型的代码实现(手撕代码):
BlockingQueue–阻塞队列
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素;当队列满时,往队列里存放数据的操作也会阻塞,直到元素从队列中取出(基于不同的线程来说的)
其实就是对互斥锁 与 条件变量 的 应用
使用C++封装一个线程安全队列,也叫做阻塞队列,放满数据了就不能再放了
实现生产者与消费者模型的注意事项:
- 构造函数的作用:确定队列的最大容量数,以及条件变量与互斥锁的初始化
- 析构函数的作用: 销毁互斥锁与条件变量
- 入队操作: 提供给生产者的接口—数据入队
- 出队操作: 提供接消费者的接口—数据出队
- 因为队列本身是一个临界资源,所以要对它的操作都要进行加锁操作(pthread_mutex_lock(&metex));
- 如果队列为空,消费者等待,接下来解锁(一个wait()接口完成这两步,wait()就是解锁+等待+加锁的步骤),完成出队,必须用while循环,支持并发的时候必须得判断。
- 如果队列满了才wait()让生产者等待,让生产者入队
- 当队列中有数据生产者唤醒消费者,告诉消费者有资源进行访问;当每个数据出队之后,都会唤醒生产者,告诉生产者可以生产数据了。
define MAX_QUE 10
class BlockQueue
{
//可以动态增长的队列,非安全的
//而我们现在要实现的是阻塞的线程安全队列,当我们放满了数据,那么数据就不能往里面放了,
//我们向其中添加的节点个数是有限的,所以我们要定义一个最大的节点限制capacity作为上限
std::queue<int> _queue;
int _capacity; //最大的结点数量限制,达到容量表示队列满了
//以上两个是为了实现队列而赋予的成员变量
//下面的是基于线程安全的角度实现的变量
//实现同步操作:没有数据的时候消费者要等待,数据满了的时候生产者要等待,
//所以需要条件变量,条件变量使用两个,因为不同的角色要等待在不同的条件变量上,所以要使用两个不同的条件变量
pthread_cond_t _cond_productor //生产者等待队列
pthread_cond_t _cond_consumer //消费者等待队列
pthread_mutex_t _mutex; //定义互斥锁来保证线程安全,来实现互斥
public:
BlockQueue(int que_num = MAX_QUE):_capacity(que_num) //构造函数:确定队列的最大容量数,以及条件变量与互斥锁的初始化
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_consumer,NULL);
pthread_cond_init(&_cond_productor,NULL);
}
~BlockQueue() //析构函数:销毁互斥锁与条件变量
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool QueuePush(int &data) //入队操作: 提供给生产者接口---数据入队
{
pthread_mutex_lock(&_mutex); // queue是临界资源 需要加锁保护
while(queue.size() ==capacity) //若队列中的数据已经满了,则把生产者添加到生产者等待队列中
{
pthread_cond_wait(&_cond_productor,&_mutex);
}
_queue.push(data); //数据入队
pthread_mutex_unlock(&_mutex); // 临界区临界资源queue保护完毕
pthread_cond_signal(&_cond_consumer); //解锁完毕 唤醒消费者等待队列上的消费者,队列里面有数据,通知消费者可以读取了
return true;
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex); //加锁
while (_queue.empty()) //如果队列为空
{
pthread_cond_wait(&_cond_consumer,&_mutex);//让消费者等待到消费者的等待队列
}
data = _queue.front();
_queue.pop(); //数据出队
pthread_mutex_unlock(&_mutex);//解锁
pthread_cond_signal(&_cond_productor);// 唤醒生产者队列上的生产者,可以生产数据了
return true;
};