生产者消费者模型
目录
一、生产者消费者模型的概念
二、生产者消费者模型的特点
三、生产者消费者模型优点
四、基于BlockingQueue的生产者消费者模型
4.1 基本认识
4.2 模拟实现
一、生产者消费者模型的概念
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,而通过容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器当中;消费者也不用找生产者索要数据,而是直接从这个容器中取数据。容器就类似于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器完成了生产者和消费者之间的解耦

二、生产者消费者模型的特点
- 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)
- 两种角色: 生产者和消费者(通常由进程或线程承担)
- 一个交易场所: 通常指的是内存中的一段缓冲区(可以自己通过某种方式组织)
生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?
介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此需要将该临界资源用互斥锁保护起来。所以所有生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系
生产者和消费者之间为什么会存在同步关系?
若一直让生产者生产,那么当生产者生产的数据装满容器后,生产者再生产数据就会生产失败。
反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。
注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来
三、生产者消费者模型优点
- 解耦
- 支持并发,提高效率
- 支持忙闲不均
若在主函数中调用某一函数,那么必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合
四、基于BlockingQueue的生产者消费者模型
4.1 基本认识
在多线程编程中,阻塞队列(Blocking Queue)是一种常用于实现生产者消费者模型的数据结构

其与普通的队列的区别在于:
- 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素
- 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出
阻塞队列最经典的应用场景:管道
4.2 模拟实现
下面以单生产者、单消费者为例进行讲解与实现
#include <iostream>
#include <queue>
#include <pthread.h>template <class T>
class BlockQueue
{
public:BlockQueue(size_t capacity = 4) : _capacity(capacity){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);}_queue.push(data);std::cout << "Producer: " << data << std::endl;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 = _queue.front();_queue.pop();std::cout << "Consumer: " << data << std::endl;pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程}private:bool IsFull() { return _queue.size() == _capacity; }bool IsEmpty() { return _queue.empty(); }private:std::queue<T> _queue;size_t _capacity;pthread_mutex_t _mutex;pthread_cond_t _full;pthread_cond_t _empty;
};
判断是否满足生产消费条件时不能用if,而应该用while:
pthread_cond_wait函数有可能调用失败,调用失败后该执行流就会继续往后执行。为了避免出现上述情况,就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断
生产者消费者步调一致
#include <unistd.h>
#include "BlockQueue.hpp"void* Producer(void* arg)
{BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //生产者不断进行生产sleep(1);int data = rand() % 100 + 1;bq->push(data);}
}
void* Consumer(void* arg)
{int data = 0;BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //消费者不断进行消费sleep(1);bq->pop(data);}
}int main()
{pthread_t producer,consumer;BlockQueue<int>* bq = new BlockQueue<int>;pthread_create(&producer,nullptr,Producer,(void*)bq);pthread_create(&consumer,nullptr,Consumer,(void*)bq);pthread_join(producer,nullptr);pthread_join(consumer,nullptr);delete bq;return 0;
}
由于代码中生产者是每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的

生产者速度快,消费者速度慢
void* Producer(void* arg)
{BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //生产者不断进行生产int data = rand() % 100 + 1;bq->push(data);}
}
void* Consumer(void* arg)
{int data = 0;BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //消费者不断进行消费sleep(1);bq->pop(data);}
}

此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列装满。此时生产者想要再进行生产就只能在full条件变量下进行等待,直到消费者消费完一个数据后,生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了
生产者速度慢,消费者速度快
void* Producer(void* arg)
{BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //生产者不断进行生产sleep(1);int data = rand() % 100 + 1;bq->push(data);}
}
void* Consumer(void* arg)
{int data = 0;BlockQueue<int>* bq = (BlockQueue<int>*)arg;while (true) { //消费者不断进行消费bq->pop(data);}
}

虽然消费者消费的快,但开始时阻塞队列中是没有数据的,因此消费者只能在empty条件变量下等待,直到生产者生产完一个数据后,消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调就是一致的
设置唤醒策略
可以设置一些策略。譬如,下面当阻塞队列当中存储的数据大于队列容量的一半时,再唤醒消费者线程进行消费;当阻塞队列当中存储的数据小于队列容器的一半时,再唤醒生产者线程进行生产
void push(const T &data)
{pthread_mutex_lock(&_mutex);while (IsFull()) { // 不能进行生产,直到阻塞队列可以容纳新的数据pthread_cond_wait(&_full, &_mutex);}_queue.push(data);std::cout << "Producer: " << data << std::endl;if (_queue.size() >= _capacity / 2) {pthread_cond_signal(&_empty); // 唤醒在empty条件变量下等待的消费者线程}pthread_mutex_unlock(&_mutex);
}
void pop(T &data)
{pthread_mutex_lock(&_mutex);while (IsEmpty()) { // 不能进行消费,直到阻塞队列有新的数据pthread_cond_wait(&_empty, &_mutex);}data = _queue.front();_queue.pop();std::cout << "Consumer: " << data << std::endl;if (_queue.size() <= _capacity / 2) {pthread_cond_signal(&_full); // 唤醒在full条件变量下等待的生产者线程}pthread_mutex_unlock(&_mutex);
}

仍然让生产者生产快,消费者消费慢。运行代码后生产者还是一瞬间将阻塞队列装满后进行等待,但此时不是消费者消费一个数据就唤醒生产者线程,而是当阻塞队列当中的数据小于等于队列容器的一半时,才会唤醒生产者线程进行生产
基于任务的生产者消费者模型
实际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,前面的代码只是为了理解生产者消费者模型而已。
编写BlockingQueue时当中存储的数据就进行了模板化,那么就可以让BlockingQueue当中存储其他类型的数据。
譬如编写一个Task类(其中包含需要执行的任务),BlockingQueue中就存储Task对象。此时生产者放入阻塞队列的数据就是Task对象,而消费者从阻塞队列拿到Task对象后,就可以用该对象调用Run成员函数进行数据处理。
总之,根据需要进行编写即可
相关文章:
生产者消费者模型
目录 一、生产者消费者模型的概念 二、生产者消费者模型的特点 三、生产者消费者模型优点 四、基于BlockingQueue的生产者消费者模型 4.1 基本认识 4.2 模拟实现 一、生产者消费者模型的概念 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题 生产者和…...
mysql索引--实例
学生表:Student (Sno, Sname, Ssex , Sage, Sdept) 学号,姓名,性别,年龄,所在系 Sno为主键 课程表:Course (Cno, Cname,) 课程号,课程名 Cno为主键 学生选课表:SC (Sno, Cno, Score)…...
浅聊一下,可中断锁(ReentrantLock)
前言 今天早上上厕所,上的我痔疮犯了,屁股一坐下去就感觉一根针在刺我,得的是外痔,之前还坚持用痔疮膏来着,但是感觉涂药的那个姿势以及位置我实在无法忍受,就把它给断了,到头来还是屁股糟了罪&…...
关于Arcgis林业数据处理的62个常用技巧
一、计算面积 ( 可以帮我们计算小班面积 ) 添加 AREA 字段,然后右键点击字段列,然后点击 CALCULATE VALUES; ---> 选择 ADVANCED --》把下面的代码输入,然后在最下面 处写 OUTPUT 点击 OK 就 OK 了。 Dim Outp…...
一些NLP术语
一些NLP术语pre-training(预训练)fine-tuning(微调)下游任务Few-shot Learning(少样本学习)Prompt?(自然语言提示信息)二级标题三级标题pre-training(预训练&…...
Session详解,学习 Session对象一篇文章就够了
目录 1 Session概述 2 Session原理 3 Session使用 3.1 获取Session 3.2 Session保存数据 3.3 Session获取数据 3.4 Session移除数据 4 Session与Request应用区别 4.1 Session和request存储数据 4.2 获取session和request中的值 4.3 session和request区别效果 5 Sess…...
Java——不同的子序列
题目链接 leetcode在线oj题——不同的子序列 题目描述 给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新…...
Git 基本操作之Git GUI界面和git命令行如何选择
1. 为啥推荐使用git命令行 我发现公司有很多的同事都喜欢使用git的GUI界面工具,喜欢鼠标点点点就完成了代码的提交,这种方式的确是比较简单便捷,但是却存在风险。先上一个事故给大家醒醒脑。 VScode Git 界面操作引发的惨案 上面的惨案是VS…...
Python编程 动态爱心
作者简介:一名在校计算机学生、每天分享Python的学习经验、和学习笔记。 座右铭:低头赶路,敬事如仪 个人主页:网络豆的主页 目录 前言 一.所用库 1.random简介 2.math 简介 3.tkinter库的简介 二.实际图 三.…...
JavaScript :基础语法
位置: HTML 中的 Javascript 脚本代码必须位于 <script> 与 </script> 标签之间。 JavaScript 输出方式 window.alert() 弹出警告框。document.write() 将内容写到 HTML 文档中。innerHTML 写入到 HTML 元素。console.log() 写入到浏览器的控制台。 …...
buu [AFCTF2018]Single 1
题目描述: Jmqrida rva Lfmz (JRL) eu m uqajemf seny xl enlxdomrexn uajiderc jxoqarerexnu. Rvada mda rvdaa jxooxn rcqau xl JRLu: Paxqmdyc, Mrrmjs-Yalanja mny oekay. Paxqmdyc-urcfa JRLu vmu m jxiqfa xl giaurexnu (rmusu) en dmnza xl jmrazxdeau. Lxd …...
Linux C++ 200行完成线程池类
文章目录1、atomic使用2、volatile关键字3、条件变量4、成员函数指针使用5、线程池6、主线程先退出对子线程影响7、return、exit、pthread_exit区别8、进程和线程的区别1、atomic使用 原子操作,不可分割的操作,要么完整,要么不完整。 #includ…...
C语言指针剖析(初阶) 最详细!
什么是指针?指针和指针类型野指针指针运算指针和数组二级指针指针数组什么是指针?指针是内存中一个最小单元的编号,也就是地址。1.把内存划分为一个个的内存单元,一个内存单元的大小是一个字节。2.每个字节都给定唯一的编号&#…...
AcWing语法基础课笔记 第三章 C++中的循环结构
第三章 C中的循环结构 学习编程语言语法是次要的,思维是主要的。如何把头脑中的想法变成简洁的代码,至关重要。 ——闫学灿 学习循环语句只需要抓住一点——代码执行顺序! while循环 可以简单理解为循环版的if语句。If语句是判断一次…...
A simple freeD tracking protocol implementation written in golang
可以使用的go版本freed调试代码 可以通过udp发送和接收数据 What is freeD? freeD is a very simple protocol used to exchange camera tracking data. It was originally developed by Vinten and is now supported by a wide range of hard- and software including Unreal…...
简约精美电商小程序【源码好优多】
简介 一款开源的电商系统,包含微信小程序和H5端,为大中小企业提供移动电子商务优秀的解决方案。 后台采用Thinkphp5.1框架开发,执行效率、扩展性、稳定性值得信赖。并且Jshop小程序商城上手难度低,可大量节省定制化开发周期。 功…...
全网详解 .npmrc 配置文件:比如.npmrc的优先级、命令行,如何配置.npmrc以及npm常用命令等
文章目录1. 文章引言2. 简述.npmrc3. 配置.npmrc3.1 .npmrc配置文件的优先级3.2 .npmrc设置的命令行3.3 如何设置.npmrc4. 配置发布组件5. npm常用命令6. 重要备注6.1 yarn6.2 scope命名空间6.3 镜像出错1. 文章引言 今天在某低代码平台开发项目时,看到如下编译配置…...
从0开始学python -31
Python3 模块-1 在前面的几个章节中我们基本上是用 python 解释器来编程,如果你从 Python 解释器退出再进入,那么你定义的所有的方法和变量就都消失了。 为此 Python 提供了一个办法,把这些定义存放在文件中,为一些脚本或者交互…...
Jenkins的使用教程
介绍: Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。 目的: 最重要目的就是把原来分散在各个机器上繁杂的工作全部…...
1.Maven的坐标和依赖
【maven坐标】1.groupId: 通常与域名反向一一对应2.artifactId: 通常使用实际项目名称3.version: 项目当前版本号4.packaging:maven项目的打包方式,默认是jar5.classifier: 定义构建输出的一些附属构件,例如:nexus-indexer-2.0.0.…...
CANopen协议核心机制与工业自动化应用实践
1. CANopen协议的核心机制解析 CANopen协议作为工业自动化领域的通信标准,其核心在于三个关键机制:对象字典、网络管理(NMT)以及过程数据对象(PDO)/服务数据对象(SDO)。理解这些机制…...
GPTMessage项目拆解:SwiftUI+Combine集成OpenAI与Hugging Face API实战
1. 项目概述与核心价值最近在折腾一个挺有意思的Side Project,一个叫GPTMessage的iOS/macOS应用。简单来说,它把ChatGPT的聊天能力、DALLE的图像生成,还有Hugging Face上的一些模型(比如图像描述、Stable Diffusion)给…...
德尔·考德威尔:从微波校准到计量标准,塑造现代精密测量的隐形基石
1. 一位计量学巨匠的遗产:从德尔考德威尔看精密测量的基石在电子工程与测试测量这个庞大而精密的领域里,我们常常关注的是最新的示波器带宽、最前沿的矢量网络分析技术,或是某个芯片的测试方案。然而,支撑起整个现代工业测量体系可…...
基于FastAPI与Flutter的LLM全栈聊天应用:私有化部署与架构解析
1. 项目概述与核心价值最近在折腾一个全栈的AI聊天应用,把后端、前端、数据库和缓存都整合到了一起。这个项目叫LLMChat,它不是一个简单的API包装器,而是一个功能完备、可以私有化部署的聊天平台。核心是用Python的FastAPI构建高性能后端&…...
从“Hello There!”徽章看低功耗Mesh网络在嵌入式社交硬件的实现
1. 项目概述:当硬件徽章成为社交网络的物理层如果你参加过大型的技术会议,尤其是像嵌入式系统大会(ESC)这样的场合,你肯定对那种既兴奋又略带尴尬的社交氛围不陌生。满屋子都是聪明绝顶的工程师,大家脑子里…...
航空摇篮长岛:从早期飞行到现代航空工业的技术演进与创新集群
1. 项目概述:从长岛的天空回望航空摇篮如果你对航空史感兴趣,或者像我一样,是个对机械、工程和人类如何突破物理极限着迷的工程师,那么“长岛”这个名字绝对绕不开。它不仅仅是纽约市旁边的一个地理名词,在航空史上&am…...
Flutter 轻量存储方案介绍、区别、对比和使用场景
在 Flutter 项目中,本地存储通常可以分为几类: 第一类是轻量 Key-Value 存储,例如 shared_preferences、get_storage、mmkv,适合保存开关、配置、登录状态等简单数据。 第二类是安全存储,例如 flutter_secure_storage&…...
独立开发者实战:AI编程的泥泞战壕与生存指南
1. 从“氛围编程”到真实战场:一个独立开发者的自白如果你最近也在关注独立开发或者AI编程工具,那你一定听过“氛围编程”这个词。它听起来很酷,对吧?仿佛你只需要对着AI描述一下心中的“氛围感”,一个完美的应用就能应…...
基于MCP的AI智能体:用自然语言轻松管理TikTok广告投放
1. 项目概述:用AI智能体玩转TikTok广告投放 如果你正在做跨境电商、品牌出海,或者任何面向年轻消费者的生意,TikTok广告绝对是你绕不开的战场。但真正上手后,你会发现事情没那么简单:TikTok的广告后台(Ads…...
C++ 入门核心语法|从 Hello World 到基础特性一次性吃透
文章目录前言一、C 第一个程序:Hello World二、命名空间 namespace1. 为什么需要命名空间?2. 命名空间定义规则3. 三种使用方式三、C 输入 & 输出1. 核心对象2. 最大优势四、缺省参数(默认参数)1. 定义2. 使用方式3. 声明与定…...
