1. 引入
生产消费模型,是一种同步关系,是多进程、多线程,同步互斥的一种场景、策略
简称为cp问题,Producer - Consumer。
以日常生活的超市为例:

消费者不需要去等待工厂的生产,而可以直接在超市购买;同样地,生产者也不需要等待消费者前来购买,生产出来后,直接存放到超市即可。
超市的存在,带来了明显的优点:
1.1 优势一:缓存
有了超市之后,效率明显地提高,超市就像一个大号的缓存。
对于这个缓存,供货商关注的是空间;而消费者关注的是商品数。
因为缓存的存在,本质上,调整了供货商和消费者之间
因为速度不一致,而带来的效率问题
1.2 优势二:支持忙闲不均
同时有了超市的存在,支持忙闲不均:
比如过年时,供货商要放假,但是消费者要消费,
没关系,有超市的存在,供货商提前生产好商品,
存储在超市,消费者消费时,去超市消费即可
1.3 优势三:解耦
有了超市的存在,生产和消费的行为,进行了一定程度的解耦。
供货商在生产商品时,消费者是不关心的;
消费者在使用商品时,供货商也是不关心的
多线程强调并发,并发就要强调解耦,解耦工作做好,并发度才能更好。
2. 实际应用:多线程
实际的应用下,生产者和消费者的角色,都是由线程来承担的。

生产、消费的过程,本质就是,执行流在做通信。
我们关注的重点是,如何安全高效的通信。
2.1 共享资源的并发问题
内存空间,作为C、P的共享资源,访问时,存在着并发问题。
三种关系:
2.1.1 生产者 vs 生产者
生产者 和 生产者 之间,要保证互斥关系。
供货商之间,比如康师傅和统一,二者就是竞争关系 ,理想情况是,只有我一家存在。但是不太可能做到。
但至少要保证,一家供货商,在超市供货时,别的厂家不能进行供货,要等待。
二者是纯竞争关系,就不需要保证顺序了。
2.1.2 消费者 vs 消费者
消费者 和 消费者 之间,也要保证互斥关系。
补充:把超市看作100个展柜,那么C1使用1展柜,C2使用2展柜,不就不冲突了吗。
目前就把超市看作整体,不考虑这种情况。
2.1.3 生产者 vs 消费者
生产者 和 消费者 之间,要保证互斥和同步关系。
互斥主要是为了保证安全。
摆放(生产)商品和拿取商品的操作,必须是原子的。
不能存在不确定性,比如消费者是否拿到商品取决于,商品是否被摆在了柜台上,可能已经摆上去了,也可能没有,这就会导致不确定性,要避免这种情况。
保证同步。
同时要保证同步,如果供货时频繁去访问超市,消费者就无法访问了,因为要保证二者的互斥,就会导致消费者饥饿问题。所以生产和消费,要有一定顺序性。
比如供货商打电话,进行供货,打完这次之后,就不能再打了。
比如要求5天后再打,从而保证消费者的电话能打进来。反之亦是如此
2.2 总结
我们可以使用321原则,概括生产者、消费者模型。
3:3种关系,C和C、P和P、C和P。
2:2种角色,生产者和消费者。注意,每种角色可能有多个
1:1个交易场所,也就是特定结构的内存空间。
优点:
一、支持忙闲不均
二、生产和消费进行解耦
2.2.1 关于耦合
生产和消费,只会通过交易的中间场所,产生一定的耦合,其他情况下,是互不干扰的。

3. 基于BlockingQueue的生产者消费者模型

3.1 代码实现
借助C++的queue,模拟阻塞队列的生产消费模型
队列有上限,也可以为空
单生产者、单消费者的代码如下:
cpp
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;
template <class T>
class BlockQueue
{
public:
BlockQueue(int max_size = 5) : _max_size(max_size)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_p_cond, nullptr);
pthread_cond_init(&_c_cond, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
}
void push(const T &data)
{
pthread_mutex_lock(&_mutex);
if (_q.size() == _max_size) // 可以写成循环?实际上,一定是队列有空位置时,才唤醒生产者
{
pthread_cond_wait(&_p_cond, &_mutex); // 队列满了之后,就不能进行生产了
}
// 1.队列不为满 2.被唤醒并且拿到锁资源
_q.push(data);
// 此时队列一定被生产了一个数据,此时可以唤醒消费者
pthread_cond_signal(&_c_cond);
pthread_mutex_unlock(&_mutex);
}
T pop()
{
pthread_mutex_lock(&_mutex);
if (_q.size() == 0)
{
pthread_cond_wait(&_c_cond, &_mutex); // 队列空了之后,就不能进行消费了
}
// 1.队列不为空 2.被唤醒并且拿到锁资源
T data = _q.front();
_q.pop();
// 此时队列一定被消费了一个数据,此时可以唤醒生产者
pthread_cond_signal(&_p_cond);
pthread_mutex_unlock(&_mutex); // 正确的解锁位置
return data;
}
private:
queue<T> _q; // 所访问的共享资源
pthread_mutex_t _mutex;
pthread_cond_t _p_cond;
pthread_cond_t _c_cond;
int _max_size; // 极值,规定的队列最大容量
};
//main.cpp
#include "BlockQueue.hpp"
#include <unistd.h>
void *Produce(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
int data = 1;
while (1)
{
bq->push(data);
cout << "生产了一个数:" << data << endl;
data++;
// sleep(1);
}
}
void *Consume(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
while (1)
{
sleep(1);
int data = bq->pop();
cout << "消费了一个数:" << data << endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>;
pthread_t p, c;
pthread_create(&p, nullptr, Produce, bq);
pthread_create(&c, nullptr, Consume, bq);
// 主线程进行等待
pthread_join(p, nullptr);
pthread_join(c, nullptr);
delete bq;
return 0;
}
如图:

3.2 注意事项


3.2.1 提问

链接:豆包的解答

3.3 添加高、低水位线

