【Linux】生产者消费者模型 - 详解
目录
一.生产者消费者模型概念
1.为何要使用生产者消费者模型
2.生产者消费者之间的关系
3.生产者消费者模型的优点
二.基于阻塞队列的生产消费模型
1.在阻塞队列中的三种关系
2.BlockingQueue.hpp - 阻塞队列类
3.LockGurad.hpp - RAII互斥锁类
4.Task.hpp - 在阻塞队列中生产/消费的任务类
5.CPTest.cc - 测试
三.基于环形队列的生产消费模型
1.在环形队列中的三种关系
2.环形队列vs阻塞队列
3.CircularQueue.hpp - 环形队列类
4.LockGuard.hpp - RAII互斥锁类
5.Task.cpp - 任务类
6.CPTest.cc - 测试
四. 写在最后
1.关于条件变量的伪唤醒问题
2.关于信号量和锁的先后顺序问题
一.生产者消费者模型概念
1.为何要使用生产者消费者模型
生产者消费者模式就是通过一个"容器"来解决生产者与消费者之间的强耦合问题, 生产者和消费者彼此之间不直接通讯, 而是通过特定"容器"来进行通讯, 所以生产者生产完数据之后不需要等待消费者处理, 直接扔给"容器", 消费者不直接找生产者索要数据, 而是从"容器"中拿数据, "容器"其实就是由特定的数据结构编写的缓冲区, 平衡了生产者和消费者的处理能力, 通过生产消费之间的"容器"达到解耦的目的
2.生产者消费者之间的关系
一个场所, 也就是上述"容器"
二个角色, 生产者与消费者
三种关系
生产者 - 生产者: 互斥关系
消费者 - 消费者: 互斥关系
生产者 - 消费者: 互斥关系, 同步关系
生产者消费者模型分为两种
1.单生产单消费
2.多生产多消费
3.生产者消费者模型的优点
1.将生产者消费者的强耦合关系解耦
2.支持并发
二.基于阻塞队列的生产消费模型
1.在阻塞队列中的三种关系
生产者与生产者之间互斥
消费者与消费者之间互斥
生产者与消费者之间既互斥又同步
同步关系使用条件变量

2.BlockingQueue.hpp - 阻塞队列类
#pragma once#include <iostream>
#include <queue>
#include "LockGurad.hpp"template <class T>
class BlockingQueue
{
public:static BlockingQueue<T> *GetInst(){return &_sInst;}public:void push(const T& in){LockGurad lock(&_mutex); // RAII锁// 如果阻塞队列满了就阻塞while(isFull()){pthread_cond_wait(&_isFullCond, &_mutex); // 已满, 阻塞}_bq.push(in);pthread_cond_signal(&_isEmptyCond); // 已经添加了一个, 唤醒已空条件阻塞}void pop(T* out){LockGurad lock(&_mutex); // RAII锁// 如果阻塞队列空了就阻塞while(isEmpty()){pthread_cond_wait(&_isEmptyCond, &_mutex); // 已空, 阻塞}*out = _bq.front();_bq.pop();pthread_cond_signal(&_isFullCond); // 已经取走了一个, 唤醒已满条件阻塞}~BlockingQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_isFullCond);pthread_cond_destroy(&_isEmptyCond);}private:bool isFull() // 判满{return _bq.size() == _capacity;}bool isEmpty() // 判空{return _bq.size() == 0;}private:std::queue<T> _bq; // 阻塞队列size_t _capacity; // 阻塞队列最大容量pthread_mutex_t _mutex; // 保护阻塞队列的锁pthread_cond_t _isFullCond; // 判断是否为满pthread_cond_t _isEmptyCond; // 判断是否为空private:// 单例模式BlockingQueue(size_t capacity = 5) : _capacity(capacity){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_isFullCond, nullptr);pthread_cond_init(&_isEmptyCond, nullptr);}BlockingQueue(const BlockingQueue<T> ©) = delete;BlockingQueue<T> &operator=(const BlockingQueue<T> ©) = delete;static BlockingQueue<T> _sInst;
};template <class T>
BlockingQueue<T> BlockingQueue<T>::_sInst;
3.LockGurad.hpp - RAII互斥锁类
#pragma once#include <pthread.h>class LockGurad
{
public:LockGurad(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGurad(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};
4.Task.hpp - 在阻塞队列中生产/消费的任务类
#pragma oncetypedef int(* func_t)(int, int);struct Task
{Task(){};Task(int x, int y, func_t func):_x(x),_y(y),_func(func){}int operator()(){return _func(_x, _y);}int _x;int _y;func_t _func;
};
5.CPTest.cc - 测试
#include "BlockingQueue.hpp"
#include "Task.hpp"
#include "time.h"
#include "stdlib.h"
#include "sys/types.h"
#include "unistd.h"int myAdd(int x, int y)
{return x + y;
}void *Consumer(void *args)
{BlockingQueue<Task> *bqsInst = (BlockingQueue<Task> *)args;while (1){// 消费任务Task t;bqsInst->pop(&t);std::cout << pthread_self() << " consumer: " << t._x << "+" << t._y << "=" << t() << std::endl;sleep(1);}return nullptr;
}void *Provider(void *args)
{BlockingQueue<Task> *bqsInst = (BlockingQueue<Task> *)args;while (1){// 制作任务int x = rand() % 100 + 1;int y = rand() % 100 + 1;Task t(x, y, myAdd);// 添加任务bqsInst->push(t);std::cout << pthread_self() << " productor: " << t._x << "+" << t._y << "=?" << std::endl;sleep(1);}return nullptr;
}int main()
{srand((unsigned int)time(nullptr) ^ getpid() ^ 0x123);// 获取单例指针BlockingQueue<Task> *bqsInst = BlockingQueue<Task>::GetInst();// 单生产单消费//pthread_t c, p;//pthread_create(&c, nullptr, Consumer, bqsInst);//pthread_create(&p, nullptr, Provider, bqsInst);// 多生产多消费pthread_t c[2], p[3];pthread_create(c, nullptr, Consumer, bqsInst);pthread_create(c + 1, nullptr, Consumer, bqsInst); pthread_create(p, nullptr, Provider, bqsInst);pthread_create(p + 1, nullptr, Provider, bqsInst);pthread_create(p + 2, nullptr, Provider, bqsInst);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);pthread_join(p[2], nullptr);return 0;
}
三.基于环形队列的生产消费模型
数据结构: 环形队列, 传送入口: http://t.csdn.cn/p1OXv
1.在环形队列中的三种关系
生产者与生产者之间互斥
消费者与消费者之间互斥
生产者与消费者之间同步
同步关系使用信号量
生产消费不需要互斥, 即分别用两把锁

2.环形队列vs阻塞队列
环形队列对比阻塞队列的优势: 生产与消费可以同时进行, 这是因为环形数据结构的特性, 以及信号量sem共同支持的
为什么?
对于阻塞队列而言
当阻塞队列中还存在有数据时, 生产者与消费者必然不会访问到同一个数据, 但是当队列为空, 如果不将生产与消费互斥的话, 就很可能在生产的同时又在消费, 消费的可能是正在生产中的数据, 就可能发生问题
对于环形队列而言
生产者与消费者的下标, 只有在环形队列满或空时指向同一位置, 通过信号量进行同步之后, 不可能会发生同时在同一位置生产消费的情况
生产消费指向同一位置, 只有两种情况
1.环形队列为满, 此时该位置是还存有未消费的数据, 没有多余的生产信号量, 该位置只可能存在消费行为
2.环形队列为空, 此时该位置不存有任何数据, 没有多余的消费信号量, 该位置只可能存在生产行为
3.CircularQueue.hpp - 环形队列类
#include <vector>
#include <semaphore.h>
#include "LockGurad.hpp"template <class T>
class CircularQueue
{
public:CircularQueue(size_t capacity) : _capacity(capacity), _cStep(0), _pStep(0){_v.resize(capacity);sem_init(&_spaceSem, 0, capacity);sem_init(&_dataSem, 0, 0);pthread_mutex_init(&_cMtx, nullptr);pthread_mutex_init(&_pMtx, nullptr);}void push(const T &in){// 这里把信号量加在锁前面, 进一步提高效率, 原理类似于买票看电影, 提前买票在排队检票效率更高sem_wait(&_spaceSem);{LockGurad lock(&_pMtx); // RAII加锁风格_v[_pStep++] = in;_pStep %= _capacity;}sem_post(&_dataSem);}void pop(T *out){sem_wait(&_dataSem);{LockGurad lock(&_cMtx);*out = _v[_cStep++];_cStep %= _capacity;}sem_post(&_spaceSem);}~CircularQueue(){sem_destroy(&_spaceSem);sem_destroy(&_dataSem);pthread_mutex_destroy(&_cMtx);pthread_mutex_destroy(&_pMtx);}private:std::vector<T> _v; // 环形队列size_t _capacity; // 最大容量int _cStep; // 消费下标int _pStep; // 生产下标sem_t _spaceSem; // 空间资源信号量sem_t _dataSem; // 数据资源信号量pthread_mutex_t _cMtx; // 生产锁pthread_mutex_t _pMtx; // 消费锁
};
4.LockGuard.hpp - RAII互斥锁类
#pragma once#include <pthread.h>class LockGurad
{
public:LockGurad(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGurad(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};
5.Task.cpp - 任务类
#pragma oncetypedef int(* func_t)(int, int);struct Task
{Task(){};Task(int x, int y, func_t func):_x(x),_y(y),_func(func){}int operator()(){return _func(_x, _y);}int _x;int _y;func_t _func;
};
6.CPTest.cc - 测试
#include <iostream>
#include "CircularQueue.hpp"
#include "Task.hpp"
#include <time.h>
#include <unistd.h>
#include <stdlib.h>int myAdd(int x, int y)
{return x + y;
}void* Consumer(void* args)
{CircularQueue<Task>* cq = (CircularQueue<Task>*)args;while(1){// 消费任务Task t;cq->pop(&t);std::cout << "消费: " << t._x << "+" << t._y << "=" << t() << std::endl;}
}void* Provider(void* args)
{CircularQueue<Task>* cq = (CircularQueue<Task>*)args;while(1){// 制作任务int x = rand() % 50 + 1;int y = rand() % 50 + 1;Task t(x, y, myAdd);// 添加任务cq->push(t);std::cout << "生产: " << t._x << "+" << t._y << "=?" << std::endl;sleep(1);}
}int main()
{srand((unsigned int)time(nullptr) ^ 0x123);CircularQueue<Task>* cq = new CircularQueue<Task>(5);pthread_t c[2], p[3];pthread_create(c, nullptr, Consumer, cq);pthread_create(c+1, nullptr, Consumer, cq);pthread_create(p, nullptr, Provider, cq);pthread_create(p+1, nullptr, Provider, cq);pthread_create(p+2, nullptr, Provider, cq);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);pthread_join(p[2], nullptr);return 0;
}
四. 写在最后
1.关于条件变量的伪唤醒问题

为什么不用if, 而使用while, 原因就是如果此时只有1个资源, 而刚刚有很多个线程在挂起等待, 为了这1个资源全部被唤醒了, 那么就一定只有一个线程真正的拿到资源, 其他线程都是伪唤醒, 避免这种问题的方法就是用while判断, 当线程被唤醒时再循环判断一次, 如果为假就会跳出循环, 如果为真说明是伪唤醒, 则继续挂起等待
2.关于信号量和锁的先后顺序问题
值得一提的是, 环形队列中生产者消费者的同步关系由信号量来实现, 并且生产者与消费者之间没有互斥关系, 那么生产者与消费者分别使用两把锁维护其各自的互斥关系, 那么有一个值得思考的问题就是信号量和锁谁在前谁在后问题

答案是: 信号量先申请比较好. 信号量确实访问了临界资源, 但其内部实现也是原子性的, 而且信号量类似于预定机制, 只是提前把资源预定下来, 想要访问资源, 只需要等待取走锁资源即可
相关文章:
【Linux】生产者消费者模型 - 详解
目录 一.生产者消费者模型概念 1.为何要使用生产者消费者模型 2.生产者消费者之间的关系 3.生产者消费者模型的优点 二.基于阻塞队列的生产消费模型 1.在阻塞队列中的三种关系 2.BlockingQueue.hpp - 阻塞队列类 3.LockGurad.hpp - RAII互斥锁类 4.Task.hpp - 在阻塞队…...
源码深度解析Spring Bean的加载
在应用spring 的过程中,就会涉及到bean的加载,bean的加载经历一个相当复杂的过程,bean的加载入口如下: 使用getBean()方法进行加载Bean,最终调用的是AbstractBeanFactory.doGetBean() 进行Bean的…...
STL——priority_queue
一、priority_queue介绍及使用 1.priority_queue文档介绍 (1)优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。 (2)此上下文类似与堆,在堆中可以…...
Springboot集成工作流Activity
介绍 官网:https://www.activiti.org/ 一 、工作流介绍 1.工作流(workflow) 就是通过计算机对业务流程自动化执行管理,它主要解决的是“使在多个参与这之间按照某种预定义规则自动化进行传递文档、信息或任务的过程,…...
2023软件测试工程师涨薪攻略,3年如何达到月薪30K?
1.软件测试如何实现涨薪 首先涨薪并不是从8000涨到9000这种涨薪,而是从8000涨到15K加到25K的涨薪。基本上三年之内就可以实现。 如果我们只是普通的有应届毕业生或者是普通本科那我们就只能从小公司开始慢慢往上走。 有些同学想去做测试,是希望能够日…...
Java面试——Spring Bean相关知识
目录 1.Bean的定义 2.Bean的生命周期 3.BeanFactory及Factory Bean 4.Bean的作用域 5.Bean的线程安全问题 1.Bean的定义 JavaBean是描述Java的软件组件模型。在Java模型中,通过JavaBean可以无限扩充Java程序的功能,通过JavaBean的组合可以快速的生…...
上班在群里摸鱼,逮到一个字节8年测试开发,聊过之后羞愧难当...
老话说的好,这人呐,一旦在某个领域鲜有敌手了,就会闲得某疼。前几天我在上班摸鱼刷群的时候认识了一位字节测试开发大佬,在字节工作了8年,因为本人天赋比较高,平时工作也兢兢业业,现在企业内有一…...
HTTP、WebSocket和Socket.IO
一、HTTP协议 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)。HTTP 协议和 TCP/IP 协议族内的其他众多的协议相同, 用于客户端和服务器之间的通信。请求访问文本或图像等资源的一端称为客户端, 而提供资源响应的一端称…...
Fluent Python 笔记 第 11 章 接口:从协议到抽象基类
本章讨论的话题是接口:从鸭子类型的代表特征动态协议,到使接口更明确、能验证实现是否符合规定的抽象基类(Abstract Base Class,ABC)。 11.1 Python 文化中的接口和协议 对 Python 程序员来说,“X 类对象”“X 协 议”和“X 接口”都是一个…...
【Spark分布式内存计算框架——Spark Core】11. Spark 内核调度(下)
8.5 Spark 基本概念 Spark Application运行时,涵盖很多概念,主要如下表格: 官方文档:http://spark.apache.org/docs/2.4.5/cluster-overview.html#glossary Application:指的是用户编写的Spark应用程序/代码&#x…...
Java中的函数
1.String.trim() : 主要有2个用法: 1、就是去掉字符串中前后的空白;这个方法的主要可以使用在判断用户输入的密码之类的。 2、它不仅可以去除空白,还可以去除字符串中的制表符,如 ‘\t’,\n等。 2.Integer.parseInt() : 字符串…...
实验6-霍纳法则及变治技术
目录 1.霍纳法则(Horners rule) 2.堆排序 3.求a的n次幂 1.霍纳法则(Horners rule) 【问题描述】用霍纳法则求一个多项式在一个给定点的值 【输入形式】输入三行,第一行是一个整数n,表示的是多项式的最高次数;第二行多项式的系数组P[0...n](从低到高存储);第三行是…...
IP地址:揭晓安欣警官自证清白的黑科技
《狂飙》这部电视剧,此从播出以来可谓是火爆了,想必大家都是看过的。剧中,主人公“安欣”是一名警察。一直在与犯罪分子做斗争。 莽村的李顺案中,有匿名者这个案件在网上发帖恶意造谣,说安欣是黑恶势力的保护伞&#…...
考研复试机试 | C++
目录1.盛水最多的容器<11>题目代码:2.整数转罗马数字题目:代码:3. 清华大学机试题 abc题目题解4.清华大学机试题 反序数题目描述代码对称平方数题目代码:5. 杭电上机题 叠筐题目:代码pass:关于清华大…...
第四章.误差反向传播法—误差反向传播法实现手写数字识别神经网络
第四章.误差反向传播法 4.3 误差反向传播法实现手写数字识别神经网络 通过像组装乐高积木一样组装第四章中实现的层,来构建神经网络。 1.神经网络学习全貌图 1).前提: 神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称…...
IB学习者的培养目标有哪些?
IB课程强调要培养年轻人的探究精神,在富有渊博知识的同时,更要勤于思考,敢于思考,尊重和理解跨文化的差异,坚持原则维护公平,让这个世界充满爱与和平,让这个世界变得更加美好。上一次我们为大家…...
C++类基础(十三)
类的继承 ● 通过类的继承(派生)来引入“是一个”的关系( 17.2 — Basic inheritance in C) – 通常采用 public 继承( struct V.S. class ) – 注意:继承部分不是类的声明 – 使用基类的指针…...
03 OpenCV图像运算
文章目录1 普通加法1 加号相加2 add函数2 加权相加3 按位运算1 按位与运算2 按位或运算、非运算4 掩膜1 普通加法 1 加号相加 在 OpenCV 中,图像加法可以使用加号运算符()来实现。例如,如果要将两幅图像相加,可以使用…...
【C语言学习笔记】:动态库
一、动态库 通过之前静态库那篇文章的介绍。发现静态库更容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢? 1、为什么还需要动态库? 为什么需要动态库,其实也是静态库的特点导致。 ▶ 空间浪费是静…...
Zookeeper
zookeeper是一个分布式协调服务。所谓分布式协调主要是来解决分布式系统中多个进程之间的同步限制,防止出现脏读,例如我们常说的分布式锁。 zookeeper中的数据是存储在内存当中的,因此它的效率十分高效。它内部的存储方式十分类似于文件存储…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
