目录
本文说明
多线程这个章节,博客共5篇,这是第三篇,介绍生产者消费者模型
一:概念
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
换句话说:生产者消费者不直接打交道,全靠一个"中间站"传东西。生产者干完活,把东西往中间站一扔就不用管了,继续干下一个活。消费者需要东西了,也不用去问生产者,自己去中间站拿就行。这个中间站最大的好处就是:让生产者和消费者不用互相等着,能各干各的!
1:特点
生产者消费者模型,简称"三二一",其特点如下:
- 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
- 两种角色: 生产者和消费者。(通常由进程或线程承担)
- 一个交易场所: 通常指的是内存中的一段缓冲区。(一般是某种数据结构)
解释:
生产者和生产者之间互斥是必然的,因为不能我写入数据的时候,你也访问临界区一起写入数据
消费者和消费者之间互斥是必然的,因为不能我拿数据的时候,你也访问临界区一起拿数据
生产者和消费者之间互斥是必然的,因为不能我写入数据的时候,你还访问临界区拿走数据
生产者和消费者之间同步是必然的,因为避免线程饥饿问题,导致一直写入数据或者拿数据
此外,生产者和生产者,消费者和消费者之间也可以实现同步关系,目的也是为了避免某个生产者竞争锁过强或者某个消费者竞争锁过强,但本文不实现这种关系,简化代码逻辑....
2:阻塞队列
生产者消费者模型最重要的就是交易场所,该交易场所就是阻塞队列,该阻塞队列的特点是:
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
换句话说:
- 让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再尝试生产就会阻塞!
- 反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会阻塞!
- 而生产者和消费者是具有同步机制的,所以不存在过久的容器空或容器满的情况
3:生活中的模型
超市是现实世界中最经典的生产者-消费者模型:
概念 | 生产者-消费者模型 | 超市模型 |
---|---|---|
生产者 | 生产数据的线程 | 供应商/送货员 (如可口可乐货车、面包厂司机) |
消费者 | 消费数据的线程 | 购物顾客 |
缓冲区 | 共享的数据结构(如队列) | 超市的货架/仓库 |
商品 | 数据/任务 | 货架上的商品 (如可乐、面包) |
互斥锁(Mutex) | 保证同时只有一个线程访问缓冲区 | 货架的通道/规则 (一次只能一个人购物,或者一个厂家放货) |
条件变量 | 线程间发送通知和等待信号 | 库存管理系统 |
› 缓冲区满时(cond_full ) |
生产者等待(pthread_cond_wait ) |
送货员看到货架已满,停下来等待店员通知有空位 |
› 生产者放入后(cond_signal ) |
通知消费者(pthread_cond_signal ) |
店员告诉顾客:"新货到了!" |
› 缓冲区空时(cond_empty ) |
消费者等待(pthread_cond_wait ) |
顾客看到货架空了,询问店员何时补货 |
› 消费者取走后(cond_signal ) |
通知生产者(pthread_cond_signal ) |
顾客买走商品,店员告诉送货员:"有空位了,可以补货了!" |
在我们实现代码的过程中,谁通知生产者阻塞队列有空位呢?谁又通知消费者阻塞队列有数据呢?
很显然:
生产者生产数据后,此时可以让生产者去通知消费者有数据了!
消费者消费数据后,此时可以让消费者去通知生产者有空位了!
换句话说:
生产者生产数据后,就去唤醒消费者
消费者消费数据后,就去唤醒生产者
4:模型的优点
优点共三点:
- 解耦。
- 支持并发。
- 支持忙闲不均。
解耦:就是让生产者和消费者"不直接打交道",互相不认识、不依赖。
支持并发:生产者和消费者可以同时各自独立地工作,而不是一个必须等另一个干完活才能开始。
支持忙闲不均:能平衡生产者和消费者双方的速度差异。一方干得快,另一方干得慢,没关系,中间有个阻塞队列可以存储数据
二:实现阻塞队列
1:代码
BlockQueue.hpp整体代码及其注释如下:
cpp
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
#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); // 则push进数据
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(); // 然后再把数据从队首pop掉
pthread_mutex_unlock(&_mutex); // 再释放锁
pthread_cond_signal(&_full); // 此时有空位了,则唤醒在full条件变量下等待的生产者线程
}
private:
std::queue<T> _q; // 队列
int _cap; // 阻塞队列的容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件为满时 在此条件变量所维持的队列下等待
pthread_cond_t _empty; // 条件为空时 在此条件变量所维持的队列下等待
};
2:解释
①:成员变量
cpp
private:
std::queue<T> _q; // 队列
int _cap; // 阻塞队列的容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件为满时 在此条件变量所维持的队列下等待
pthread_cond_t _empty; // 条件为空时 在此条件变量所维持的队列下等待
解释:
阻塞队列的核心肯定是一个队列,只不过我们对其的行为稍加限制罢了;
我们还需要一个变量来标识阻塞队列的容量;
仅需一把锁,这样生产者之间是互斥的,消费者之间是互斥的,生产者消费者之间是互斥的;
需要两个成员变量,_full用于维持生产者的等待队列,_empty用于维持消费者的等待队列,这样我们就能实现生产者和消费者之间的同步了
②:成员函数
a:判空和判满函数
cpp
bool IsFull() // 判断是否为满 满则返回真
{
return _q.size() == _cap;
}
bool IsEmpty() // 判断是否为空 空则返回真
{
return _q.empty();
}
**解释:**判断空满的函数,本质是调用成员变量q这个队列的成员函数
b:构造和析构
cpp
// 构造函数
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); // 销毁条件变量
}
**解释:**因为有锁和条件变量,所以构造函数和析构函数需要调用专用的接口就是初始化和销毁
c:push函数
cpp
//向阻塞队列插入数据(生产者调用)
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条件变量下等待的消费者线程
}
解释:
push函数必定是生产者调用的函数;
申请到锁之后,如果阻塞队列不为满,则生产者会向队列中push进数据,然后释放锁,然后唤醒在_empty下等待的消费者线程
申请到锁之后,如果阻塞队列为满,则该生产者线程需要在_full这个条件变量所维持的队列下等待,直到被消费者消费数据后导致阻塞队列不为满,才会被唤醒去生产数据,然后释放锁,然后再去唤醒再_empty下等待的消费者线程
d:pop函数
cpp
//从阻塞队列获取数据(消费者调用)
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); //唤醒在full条件变量下等待的生产者线程
}
**解释:**同理,不再赘述
3:伪唤醒
push和pop函数中的while是及其重要的,用if则必定会出错!
cpp
//向阻塞队列插入数据(生产者调用)
void Push(const T& data)
{
pthread_mutex_lock(&_mutex);
if (IsFull())// if必定会出错!
{
//不能进行生产,直到阻塞队列可以容纳新的数据
pthread_cond_wait(&_full, &_mutex);
}
_q.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
}
解释:
①: pthread_cond_wait
函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。
②: 即使pthread_cond_wait
函数调用每出错,但是我们知道在条件变量所维持的队列中等待的线程,在唤醒之后,会从当前代码往下执行,这意味着,如果是if,其会直接向下执行直接去生产数据,但是有可能会存在即使生产者被唤醒,但阻塞队列仍然为满的情况,这就会导致生产者向一个满的阻塞队列中强行的push数据,必定会出错,并且这样未被了我们阻塞队列的逻辑!所以用wile之后,我们相当于加了一层安全保险,再也不用担心这种情况1
这种情况被称作:伪唤醒!
被唤醒的线程实则仍然未满足被唤醒的条件,但是却被唤醒了!
③:伪唤醒的例子:
现有1个生产者,5个消费者。生产者值生产了一个数据,但是却唤醒了全部消费者,这对于剩下的4个消费者而言,数据已经被第一个消费者去消费了,但是这4个线程仍然被唤醒了,并且现在是if的写法,则这四个线程会依次的区空的阻塞队列中强行pop数据,必然出错
现有1个生产者,5个消费者,且阻塞队列现在已经有了30个数据。此时开始运行,则生产者生产一次就会唤醒个消费者,而这些唤醒对于5个消费者来说根本没用,因为队列根本不为空,消费者根本没有在等待队列,所以就变成了生产者和消费者之间进行同步的去生产消费,直到等列为空,消费者才会去等待,但是之前累计的多次伪唤醒,刚好可以唤醒才开始等待的消费者,但是阻塞队列已经为空了,此时去消费,出错!
e:对唤醒的优化
所以加了while,可以避免伪唤醒造成的出错,但是仍然会发出伪唤醒,我们可以通过增加两个变量来直接根绝掉伪唤醒:
成员变量新增:
cpp
int _productor_wait_num;//表示处于等待队列中的生产者的个数
int _consumer_wait_num;//表示处于等待队列中的消费者的个数
所以构造函数需要改,push函数和pop函数也需要改,进入while循环则对应的变量++,被唤醒后,则对应的变量--即可,最终代码如下:
4:优化后的整体代码:
cpp
#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), _productor_wait_num(0), _consumer_wait_num(0) // 初始化容量
{
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()) // 如果满了
{
_productor_wait_num++; // 来到这 代表生产者必定等待 所以事先++
// 则不能进行生产,需要等待,直到阻塞队列可以容纳新的数据,才会消费者被唤醒
pthread_cond_wait(&_full, &_mutex);
_productor_wait_num--; // 唤醒后 从这开始 所以直接--
}
// 来到这 则代表队列不为满 有空位
_q.push(data); // 则push进数据
pthread_mutex_unlock(&_mutex); // 再释放锁
if (_consumer_wait_num > 0)
pthread_cond_signal(&_empty); // 此时有数据了,则唤醒在empty条件变量下等待的消费者线程
}
// 从阻塞队列获取数据(消费者调用)
void Pop(T &data)
{
pthread_mutex_lock(&_mutex); // 先申请锁
while (IsEmpty()) // 如果空了
{
_consumer_wait_num++; // 来到这 代表消费者必定等待 所以事先++
// 则不能进行消费,需要等待,直到阻塞队列有数据了,才会生产者被唤醒
pthread_cond_wait(&_empty, &_mutex);
_consumer_wait_num--; // 唤醒后 从这开始 所以直接--
}
// 来到这 则代表队列不为空 有数据
data = _q.front(); // 则取出数据
_q.pop(); // 然后再把数据从队首pop掉
pthread_mutex_unlock(&_mutex); // 再释放锁
if (_productor_wait_num > 0)
pthread_cond_signal(&_full); // 此时有空位了,则唤醒在full条件变量下等待的生产者线程
}
private:
std::queue<T> _q; // 队列
int _cap; // 阻塞队列的容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件为满时 在此条件变量所维持的队列下等待
pthread_cond_t _empty; // 条件为空时 在此条件变量所维持的队列下等待
int _productor_wait_num; // 表示处于等待队列中的生产者的个数
int _consumer_wait_num; // 表示处于等待队列中的消费者的个数
};
三:单生产者单消费者模型
1:数据为整形
mian所处的文件的代码如下:
cpp
#include "BlockQueue.hpp"
void *Producer(void *arg)
{
// 从参数得到阻塞队列
BlockQueue<int> *bq = (BlockQueue<int> *)arg;
// 生产者不断进行生产
while (true)
{
sleep(1);
int data = rand() % 100 + 1; // 生成0~100的随机数
bq->Push(data); // 生产数据
std::cout << "Producer: " << data << std::endl; // 打印生产者+生产数据
}
}
void *Consumer(void *arg)
{
// 从参数得到阻塞队列
BlockQueue<int> *bq = (BlockQueue<int> *)arg;
// 消费者不断进行消费
while (true)
{
sleep(1);
int data = 0;
bq->Pop(data); // 消费数据
std::cout << "Consumer: " << data << std::endl; // 打印消费者+消费数据
}
}
int main()
{
srand((unsigned int)time(nullptr)); // 种一颗随机数种子
pthread_t producer, consumer; // 分布存储消费者和生产者的线程ID
BlockQueue<int> *bq = new BlockQueue<int>; // new一个阻塞队列
// 创建生产者线程和消费者线程
pthread_create(&producer, nullptr, Producer, bq); // 创建一个生产者
pthread_create(&consumer, nullptr, Consumer, bq); // 创建一个消费者
// join生产者线程和消费者线程
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
delete bq;
return 0;
}
解释:我们目前是单个消费者和单个生产者,生产者生成0~100的随机数,消费者就去拿去随机数
运行效果如下:

达到了预期,因为是单生产者单消费者,所以步调必然是一致的,而打印粘合是正常现象
2:数据是函数
我们可以把阻塞队列的数据变成一个个的函数,生产者产生函数,消费者拿到函数去计算!比如加减乘除,我们的生产者获取到两个数字和计算符号之后,直接调用新建的Task类去实例化对象,此时对象之后就已经知道是什么运算法则了,然消费者再去取到阻塞队列中的对象,直接调用对象的run方法进行计算即可!
Task.hpp
cpp
#pragma once
#pragma once
#include <iostream>
#include <string>
class Task
{
public:
Task(int x = 0, int y = 0, char op = 0) // op应该是char类型
: _x(x), _y(y), _op(op)
{
}
~Task()
{
}
int Run()
{
switch (_op)
{
case '+':
return _x + _y;
case '-':
return _x - _y;
case '*':
return _x * _y;
case '/':
if (_y == 0)
{
std::cout << "Warning: div zero!" << std::endl;
return -10086;
}
return _x / _y;
case '%':
if (_y == 0)
{
std::cout << "Warning: mod zero!" << std::endl;
return -10086;
}
return _x % _y;
default:
std::cout << "error operation!" << std::endl;
return -10086;
}
}
std::string ToString() const
{
return std::to_string(_x) + " " + _op + " " + std::to_string(_y);
}
private:
int _x;
int _y;
char _op;
};
解释:
①:其中的Tostring函数负责打印x+y的前半句,然后后面再消费代码中在接上计算得到的结果
②:对于除零错误和default我们也会打印提示语句,并且把结果置为了-10086
所以main所处的文件也需要稍改一下:
cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
pthread_mutex_t cout_mutex = PTHREAD_MUTEX_INITIALIZER;
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); // 生产数据
pthread_mutex_lock(&cout_mutex);
std::cout << "生产任务:" << x << " " << op << " " << y << " = ?" << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
void *Consumer(void *arg)
{
BlockQueue<Task> *bq = (BlockQueue<Task> *)arg;
// 消费者不断进行消费
while (true)
{
sleep(1);
Task t;
bq->Pop(t); // 消费数据
pthread_mutex_lock(&cout_mutex);
int result = t.Run(); // 处理数据
std::cout << "消费任务: " << t.ToString() << " " << "=" << " " << result << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr)); // 种一颗随机数种子
pthread_t producer, consumer; // 分布存储消费者和生产者的线程ID
BlockQueue<Task> *bq = new BlockQueue<Task>; // new一个阻塞队列 类型为Task
// 创建生产者线程和消费者线程
pthread_create(&producer, nullptr, Producer, bq); // 创建一个生产者
pthread_create(&consumer, nullptr, Consumer, bq); // 创建一个消费者
// join生产者线程和消费者线程
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
delete bq;
return 0;
}
解释:除开生产数据,消费数据的代码修改了之后,我们还在打印的时候进行了加锁解锁,这会避免打印粘合的情况,对于计算式子来说,打印粘合会映射式子的准确性!
运行结果:

四:多生产者多消费者模型
很简单,我们只需多创建几个生产者和消费者即可,所以我们保存线程ID变成了一个数据,我们需要循环的创建目标个数的生产者和消费者,最后还需要循环的等待线程退出
cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
#define CONSUMER_COUNT 3
#define PRODUCER_COUNT 3
pthread_mutex_t cout_mutex = PTHREAD_MUTEX_INITIALIZER;
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); // 生产数据
pthread_mutex_lock(&cout_mutex);
std::cout << "生产任务:" << x << " " << op << " " << y << " = ?" << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
void *Consumer(void *arg)
{
BlockQueue<Task> *bq = (BlockQueue<Task> *)arg;
// 消费者不断进行消费
while (true)
{
sleep(1);
Task t;
bq->Pop(t); // 消费数据
pthread_mutex_lock(&cout_mutex);
int result = t.Run(); // 处理数据
std::cout << "消费任务: " << t.ToString() << " " << "=" << " " << result << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr)); // 种一颗随机数种子
// 存储消费者和生产者的线程ID
std::vector<pthread_t> producers(PRODUCER_COUNT);
std::vector<pthread_t> consumers(CONSUMER_COUNT);
BlockQueue<Task> *bq = new BlockQueue<Task>; // new一个阻塞队列 类型为Task
// 创建生产者线程和消费者线程
// 创建多个生产者线程
for (int i = 0; i < PRODUCER_COUNT; ++i)
{
pthread_create(&producers[i], nullptr, Producer, bq);
}
// 创建多个消费者线程
for (int i = 0; i < CONSUMER_COUNT; ++i)
{
pthread_create(&consumers[i], nullptr, Consumer, bq);
}
// join所有生产者线程
for (int i = 0; i < PRODUCER_COUNT; ++i)
{
pthread_join(producers[i], nullptr);
}
// join所有消费者线程
for (int i = 0; i < CONSUMER_COUNT; ++i)
{
pthread_join(consumers[i], nullptr);
}
delete bq;
return 0;
}
运行效果:

**解释:**因为每个生产者线程函数都是取随机数,所以多个生产者产生的任务会不同,符合预期
大胆设想一下,如果多个任务的耗时都较长,那此时用生产者消费者模型则会最高效率的执行任务,因为会有多个消费者拿到任务之后,并发的去处理任务,高效利用时间!
五:源代码
1:makefile:
cpp
test: test.cpp
g++ -std=c++11 -I. -lpthread -o $@ $^
# 添加了 -I. 选项 ^
.PHONY: clean
clean:
rm -f test
2:BlockQueue.hpp
cpp
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
#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), _productor_wait_num(0), _consumer_wait_num(0) // 初始化容量
{
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()) // 如果满了
{
_productor_wait_num++; // 来到这 代表生产者必定等待 所以事先++
// 则不能进行生产,需要等待,直到阻塞队列可以容纳新的数据,才会消费者被唤醒
pthread_cond_wait(&_full, &_mutex);
_productor_wait_num--; // 唤醒后 从这开始 所以直接--
}
// 来到这 则代表队列不为满 有空位
_q.push(data); // 则push进数据
pthread_mutex_unlock(&_mutex); // 再释放锁
if (_consumer_wait_num > 0)
pthread_cond_signal(&_empty); // 此时有数据了,则唤醒在empty条件变量下等待的消费者线程
}
// 从阻塞队列获取数据(消费者调用)
void Pop(T &data)
{
pthread_mutex_lock(&_mutex); // 先申请锁
while (IsEmpty()) // 如果空了
{
_consumer_wait_num++; // 来到这 代表消费者必定等待 所以事先++
// 则不能进行消费,需要等待,直到阻塞队列有数据了,才会生产者被唤醒
pthread_cond_wait(&_empty, &_mutex);
_consumer_wait_num--; // 唤醒后 从这开始 所以直接--
}
// 来到这 则代表队列不为空 有数据
data = _q.front(); // 则取出数据
_q.pop(); // 然后再把数据从队首pop掉
pthread_mutex_unlock(&_mutex); // 再释放锁
if (_productor_wait_num > 0)
pthread_cond_signal(&_full); // 此时有空位了,则唤醒在full条件变量下等待的生产者线程
}
private:
std::queue<T> _q; // 队列
int _cap; // 阻塞队列的容量
pthread_mutex_t _mutex; // 锁
pthread_cond_t _full; // 条件为满时 在此条件变量所维持的队列下等待
pthread_cond_t _empty; // 条件为空时 在此条件变量所维持的队列下等待
int _productor_wait_num; // 表示处于等待队列中的生产者的个数
int _consumer_wait_num; // 表示处于等待队列中的消费者的个数
};
3:test.cpp
①:单生产者单消费者
cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
pthread_mutex_t cout_mutex = PTHREAD_MUTEX_INITIALIZER;
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); // 生产数据
pthread_mutex_lock(&cout_mutex);
std::cout << "生产任务:" << x << " " << op << " " << y << " = ?" << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
void *Consumer(void *arg)
{
BlockQueue<Task> *bq = (BlockQueue<Task> *)arg;
// 消费者不断进行消费
while (true)
{
sleep(1);
Task t;
bq->Pop(t); // 消费数据
pthread_mutex_lock(&cout_mutex);
int result = t.Run(); // 处理数据
std::cout << "消费任务: " << t.ToString() << " " << "=" << " " << result << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr)); // 种一颗随机数种子
pthread_t producer, consumer; // 分布存储消费者和生产者的线程ID
BlockQueue<Task> *bq = new BlockQueue<Task>; // new一个阻塞队列 类型为Task
// 创建生产者线程和消费者线程
pthread_create(&producer, nullptr, Producer, bq); // 创建一个生产者
pthread_create(&consumer, nullptr, Consumer, bq); // 创建一个消费者
// join生产者线程和消费者线程
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
delete bq;
return 0;
}
②:多生产者多消费者
cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
#define CONSUMER_COUNT 3
#define PRODUCER_COUNT 3
pthread_mutex_t cout_mutex = PTHREAD_MUTEX_INITIALIZER;
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); // 生产数据
pthread_mutex_lock(&cout_mutex);
std::cout << "生产任务:" << x << " " << op << " " << y << " = ?" << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
void *Consumer(void *arg)
{
BlockQueue<Task> *bq = (BlockQueue<Task> *)arg;
// 消费者不断进行消费
while (true)
{
sleep(1);
Task t;
bq->Pop(t); // 消费数据
pthread_mutex_lock(&cout_mutex);
int result = t.Run(); // 处理数据
std::cout << "消费任务: " << t.ToString() << " " << "=" << " " << result << std::endl;
pthread_mutex_unlock(&cout_mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr)); // 种一颗随机数种子
// 存储消费者和生产者的线程ID
std::vector<pthread_t> producers(PRODUCER_COUNT);
std::vector<pthread_t> consumers(CONSUMER_COUNT);
BlockQueue<Task> *bq = new BlockQueue<Task>; // new一个阻塞队列 类型为Task
// 创建生产者线程和消费者线程
// 创建多个生产者线程
for (int i = 0; i < PRODUCER_COUNT; ++i)
{
pthread_create(&producers[i], nullptr, Producer, bq);
}
// 创建多个消费者线程
for (int i = 0; i < CONSUMER_COUNT; ++i)
{
pthread_create(&consumers[i], nullptr, Consumer, bq);
}
// join所有生产者线程
for (int i = 0; i < PRODUCER_COUNT; ++i)
{
pthread_join(producers[i], nullptr);
}
// join所有消费者线程
for (int i = 0; i < CONSUMER_COUNT; ++i)
{
pthread_join(consumers[i], nullptr);
}
delete bq;
return 0;
}