生产者消费者模型

news2025/1/10 11:15:38

什么是生产者消费者模型?

生产者和消费是操作系统中一种重要的模型,它描述的是一种等待和通知的机制。

一、概念引入
日常生活中,每当我们缺少某些生活用品时,我们都会去超市进行购买,那么,你有没有想过,你是以什么身份去的超市呢?相信大部分人都会说自己是消费者,确实如此,那么既然我们是消费者,又是谁替我们生产各种各样的商品呢?当然是超市的各大供货商,自然而然地也就成了我们的生产者。如此一来,生产者有了,消费者也有了,那么将二者联系起来的超市又该作何理解呢?诚然,它本身是作为一座交易场所而诞生。

将上述场景例比到我们实际的软件开发过程中,经常会见到这样一幕:代码的某个模块负责生产数据(供货商),而生产出来的数据却不得不交给另一模块(消费者)来对其进行处理,在这之间我们必须要有一个类似上述超市的东西来存储数据(超市),这就抽象除了我们的生产者/消费者模型。
其中,产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者;生产者和消费者之间的中介就叫做缓冲区。

三者之间的结构图:
在这里插入图片描述

为了方便理解,再列举一个寄信的例子:

1、你把信写好——相当于生产者制造数据
2、你把信放入邮筒——相当于生产者把数据放入缓冲区
3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据

二、为什么要使用生产者消费者模型

归根结底来说,生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。

//信号量是一个表示资源的实体,,信号量只支持P操作和V操作。其具体的意义如下述代码所示:
typedef struct Listnode{
    int data;--是一个与队列有关的整型变量
    struct Listnode *next;--链表
}node,*pnode,**ppnode;
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。

在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。

为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。

三、生产者/消费者模型及其特点
生产者消费者模型具体来讲,就是在一个系统中,存在生产者和消费者两种角色,他们通过内存缓冲区进行通信,生产者生产消费者需要的资料,消费者把资料做成产品。生产消费者模式如下图。

在日益发展的服务类型中,譬如注册用户这种服务,它可能解耦成好几种独立的服务(账号验证,邮箱验证码,手机短信码等)。它们作为消费者,等待用户输入数据,在前台数据提交之后会经过分解并发送到各个服务所在的url,分发的那个角色就相当于生产者。消费者在获取数据时候有可能一次不能处理完,那么它们各自有一个请求队列,那就是内存缓冲区了。做这项工作的框架叫做消息队列。
在这里插入图片描述

在这里插入图片描述

生产者是一堆线程,消费者是另一堆线程,内存缓冲区可以使用List数组队列,数据类型只需要定义一个简单的类就好。关键是如何处理多线程之间的协作。这其实也是多线程通信的一个范例。

在这个模型中,最关键就是:

  1. 内存缓冲区为空的时候消费者必须等待,
  2. 而内存缓冲区满的时候,生产者必须等待。
  3. 其他时候可以是个动态平衡。

值得注意的是多线程对临界区资源的操作时候必须保证在读写中只能存在一个线程,所以需要设计锁的策略。
————————————————
特点


 - 首先,生产者只需要关心“仓库”,并不需要关心具体的消费者。
 - 对于消费者而言,它不需要关心具体的生产者,它只需要关心这个“仓库”中还有没有东西存在。
 - 生产者生产的时候消费者不能进行“消费”,消费者消费的时候生产者不能生产,相当于一种互斥关系,
  即生产者和消费者一次只能有一人能访问到“仓库”。
 - “仓库”为空时不能进行消费。
 - “仓库”满时不能进行生产。

————————————————
优点

1、解耦,即降低生产者和消费者之间的依赖关系。

假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。

例如上述写信的例子,如果不使用邮筒(也就是缓区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了 )。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。

2、支持并发(concurrency),即生产者和消费者可以是两个独立的并发主体,互不干扰的运行。

生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。

从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,效率都比较低。

3、支持忙闲不均,如果制造数据的速度时快时慢,缓冲区可以对其进行适当缓冲。当数据制造快的时候,
消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。

四、生产者/消费者模型的记忆原则
为了方便记忆,我对其进行了如下总结:
三二一原则:

三种关系、两个角色、一个场所

三种关系:(生产者和消费者之间的关系相当于是一种食物链之间的依赖关系)

1生产者与生产者(互斥)
2生产者与消费者(同步与互斥)
3消费者与消费者(互斥)

两个角色:

1生产者
2消费者

一个场所:

1缓冲区

五、生产者/消费者模型的学习旅程

1、确定数据单元

★啥是数据单元
向缓冲区拿放数据的一个基本数据单元。简单地说,每次生产者放到缓冲区的,就是一个数据单元;每次消费者从缓冲区取出的,也是一个数据单元。
★数据单元的特性
◇关联到业务对象:数据单元必须关联到某种业务对象
◇完整性:保证每一个数据单元的完整
◇独立性:各个数据单元之间没有互相依赖
◇颗粒度:业务对象和数据单元之间的对应比例

2、学习队列缓冲区

★线程方式

◇内存分配的性能:内存分配的开销问题—->环形缓冲区
◇同步和互斥的性能:例如信号量、互斥量等的开销—->双缓冲区
◇适用于队列的场合:适用于数据流量不是很大的场合

★进程方式
◇匿名管道:

生产者进程在管道的写端放入数据;
消费者进程在管道的读端取出数据。

好处:

1》跨平台发方便。
2》跨语言方便。
3》有利于降低开发、调试成本。

不足:

1》生产者进程和消费者进程必须得在同一台主机上,无法跨机器通讯。
2》只适用于一对一通信。
3》在某些情况下,程序不便于对管道进行操纵(比如调整管道缓冲区尺寸)。
4》只能单向通信。

SOCKET(TCP方式)
基于TCP方式的SOCKET通讯是又一个类似于队列的IPC方式。它同样保证了数据的顺序到达;同样有缓冲的机制。而且跨平台和跨语言,和刚才介绍的shell管道符方式类似。
SOCKET相比shell管道符的方式,主要有如下几个优点:

    1、SOCKET方式可以跨机器(便于实现分布式)。这是主要优点。
    2、SOCKET方式便于将来扩展成为多对一或者一对多。这也是主要优点。
    3、SOCKET可以设置阻塞和非阻塞方法,用起来比较灵活。这是次要优点。
    4、SOCKET支持双向通讯,有利于消费者反馈信息。
  当然有利就有弊。相对于上述shell管道的方式,使用SOCKET在编程上会更复杂一些。
  好在前人已经做了大量的工作,可借助于这些第三方的库和框架,比如C++的ACE库、Python的Twisted。
  
   虽然TCP在很多方面比UDP可靠,但鉴于跨机器通讯先天的不可预料性,
   可以在生产者进程和消费者进程内部各自再引入基于线程的"生产者/消费者模式",如下图:

在这里插入图片描述
这么做的关键点在于把代码分为两部分:

生产线程和消费线程属于和业务逻辑相关的代码(和通讯逻辑无关);
发送线程和接收线程属于通讯相关的代码(和业务逻辑无关)。
这样的好处是很明显的,具体如下:
1、能够应对暂时性的网络故障。并且在网络故障解除后,能够继续工作。
2、网络故障的应对处理方式(比如断开后的尝试重连),只影响发送和接收线程,
不会影响生产线程和消费线程(业务逻辑部分)。
3、具体的SOCKET方式(阻塞和非阻塞)只影响发送和接收线程,不影响生产线程和消费线程(业务逻辑部分)。
4、不依赖TCP自身的发送缓冲区和接收缓冲区。(默认的TCP缓冲区的大小可能无法满足实际要求)
5、业务逻辑的变化(比如业务需求变更)不影响发送线程和接收线程。
针对上述的最后一条,如果整个业务系统中有多个进程是采用上述的模式,那或许可以重构:
在业务逻辑代码和通讯逻辑代码之间,把业务逻辑无关的部分封装成一个通讯中间件。

————————————————

3、学习环形缓冲区:

使用场景:

当存储空间(不仅包括内存,还可能包括诸如硬盘之类的存储介质)的分配/释放非常频繁并且确实产生了明显的影
响,才应该考虑环形缓冲区的使用。否则的话,还是选用最基本、最简单的队列缓冲区。

★环形缓冲区与队列缓冲区

**1.外部接口相似**  

普通的队列有一个写入端和一个读出端。队列为空的时候,读出端无法读取数据;当队列满(达到最大尺寸)时,写入端无法写入数据。

对于使用者来讲,环形缓冲区和队列缓冲区是一样的。它也有一个写入端(用于push)和一个读出端(用于pop),也有缓冲区“满”和“空”的状态。所以,从队列缓冲区切换到环形缓冲区,对于使用者来说能比较平滑地过渡。

2.内部结构迥异:
可以把环形缓冲区的读出端(以下简称R)和写入端(以下简称W)想象成是两个人在体育场跑道上追逐(R追W)。
当R追上W的时候,就是缓冲区为空;
当W追上R的时候(W比R多跑一圈),就是缓冲区满。
在这里插入图片描述

从上图可以看出,环形缓冲区所有的push和pop操作都是在一个固定的存储空间内进行。
而队列缓冲区在push的时候,可能会分配存储空间用于存储新元素;
在pop时,可能会释放废弃元素的存储空间。
所以环形方式相比队列方式,少掉了对于缓冲区元素所用存储空间的分配、释放。
这是环形缓冲区的一个主要优势。

★环形缓冲区的实现
1.数组方式 vs 链表方式
环形缓冲区的内部实现,即可基于数组(此处的数组,泛指连续存储空间)实现,也可基于链表实现。
数组在物理存储上是一维的连续线性结构,可以在初始化时,把存储空间一次性分配好,这是数组方式的优点。但是要使用数组来模拟环,你必须在逻辑上把数组的头和尾相连。在顺序遍历数组时,对尾部元素(最后一个元素)要作一下特殊处理。访问尾部元素的下一个元素时,要重新回到头部元素(第0个元素)。如下图所示:
在这里插入图片描述
使用链表的方式,正好和数组相反:链表省去了头尾相连的特殊处理。但是链表在初始化的时候比较繁琐,而且在有些场合(比如跨进程的IPC)不太方便使用。
2.读写操作
环形缓冲区要维护两个索引,分别对应写入端(W)和读取端(R)。
写入(push)的时候,先确保环没满,然后把数据复制到W所对应的元素,最后W指向下一个元素;
读取(pop)的时候,先确保环没空,然后返回R对应的元素,最后R指向下一个元素。
3.判断“空”和“满”
上述的操作并不复杂,不过有一个小小的麻烦:
空环和满环的时候,R和W都指向同一个位置!这样就无法判断到底是“空”还是“满”。
大体上有两种方法可以解决该问题。
办法1:始终保持一个元素不用
当空环的时候,R和W重叠。当W比R跑得快,追到距离R还有一个元素间隔的时候,就认为环已经满。当环内元素占用的存储空间较大的时候,这种办法显得很土(浪费空间)。
办法2:维护额外变量
如果不喜欢上述办法,还可以采用额外的变量来解决。比如可以用一个整数记录当前环中已经保存的元素个数(该整数>=0)。当R和W重叠的时候,通过该变量就可以知道是“空”还是“满”。
4.元素的存储

   由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储值类型的数据,而不要存储指针(引用)类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。

应用场合
如果所使用的编程语言和开发库中带有现成的、成熟的环形缓冲区,建议使用现成的库,不要重新制造轮子;确实找不到现成的,才考虑自己实现。

  1.用于并发线程

   和线程中的队列缓冲区类似,线程中的环形缓冲区也要考虑线程安全的问题。除非使用的环形缓冲区的库已经实现了线程安全,否则还是得自己动手搞定。线程方式下的环形缓冲区用得比较多,相关的网上资料也多,下面就大致介绍几个。

   对于C++的程序员,强烈推荐使用boost提供的circular_buffer模板,该模板最开始是在boost 1.35版本中引入的。鉴于boost在C++社区中的地位,大伙儿应该可以放心使用该模板。

   对于C程序员,可以去看看开源项目circbuf,不过该项目是GPL协议的,不太爽;而且活跃度不太高;而且只有一个开发人员。大伙儿慎用!建议只拿它当参考。

   对于C#程序员,可以参考CodeProject上的一个示例。

   2.用于并发进程

   进程间的环形缓冲区,似乎少有现成的库可用。

   适用于进程间环形缓冲的IPC类型,常见的有共享内存和文件。在这两种方式上进行环形缓冲,通常都采用数组的方式实现。程序事先分配好一个固定长度的存储空间,然后具体的读写操作、判断“空”和“满”、元素存储等细节就可参照前面所说的来进行。

   共享内存方式的性能很好,适用于数据流量很大的场景。但是有些语言(比如Java)对于共享内存不支持。因此,该方式在多语言协同开发的系统中,会有一定的局限性。

    而文件方式在编程语言方面支持很好,几乎所有编程语言都支持操作文件。但它可能会受限于磁盘读写(Disk I/O)的性能。所以文件方式不太适合于快速数据传输;但是对于某些“数据单元”很大的场合,文件方式是值得考虑的。

    对于进程间的环形缓冲区,同样要考虑好进程间的同步、互斥等问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node.js之Hello World

目录 简介 1.安装 2.安装后的效果 3.准备尝试运行的代码 4.运行JS代码 5.访问正在运行的代码(Hello World) 简介 Node.js 是一个基于"Chrome V8 引擎" 的JavaScript "运行环境"; NodeJS不是一门编程语言, NodeJS是一个运行环境&#xff…

C语言题解 | 消失的数字轮转数组

… 🌳🌲🌱本文已收录至:C语言题解系列 更多知识尽在此专栏中! 🎉🎉🎉欢迎点赞、收藏、关注 🎉🎉🎉文章目录🍉前言🍉正文&…

这才是使用ps命令的正确姿势

这才是使用ps命令的正确姿势 前言 在linux系统当中我们通常会使用命令去查看一些系统的进程信息,我们最常使用的就是 ps (process status)。ps 命令主要是用于查看当前正在运行的程序,以及他们相关的的信息,我们可以通过不同的选项进行查看…

全球名校AI课程库(12)| CMU卡内基梅隆 · 数据库系统进阶课程『Advanced Database Systems』

🏆 课程学习中心 | 🚧 计算机基础课程合辑 | 🌍 课程主页 | 📺 中英字幕视频 | 🚀 项目代码解析 课程介绍 CMU 15-721 是 CMU 卡内基梅隆大学开放的数据库方向进阶课程,讨论了很多数据库方向新的技术研究方…

基于Matlab使用跟踪筛选器跟踪机动目标仿真(附源码)

此示例演示如何使用各种跟踪筛选器跟踪机动目标。该示例显示了使用单个运动模型和多个运动模型的滤镜之间的差异。 一、定义方案 在此示例中,定义了一个目标,该目标最初以 200 m/s 的恒定速度行进 33 秒,然后输入 10 度/秒的恒定转弯。转弯…

【C++】类和对象(中)

类和对象是面向对象语言的一个重要部分,承接上一话,我们直接开始这一章的内容。 文章目录 一、类的6个默认成员函数二、构造函数三、析构函数四、拷贝构造函数五、赋值操作符重载六、 const成员函数七、取地址及const取地址操作符重载八、相关习题的练习…

Windows系统常用网络命令详解及命令示例(全)

1.最基本也是最常用的,PING ping 192.168.0.8 -t 测试物理网络   ,参数-t是等待用户中断测试   2.检查DNS、IP、Mac等   A. Win98:winipcfg   B.Win2000及以上:ipconfig /all      C.NSLOOKUP:如果查看河北…

初阶c语言之浅识指针

学习导航:> 1.指针是什么? 2.指针和指针类型 2.1指针-整数 2.2指针的解引用 3.野指针 3.1野指针成因 3.2如何规避野指针 4.指针运算 4.1指针-整数 4.2指针-指针 4.3指针的关系运算 5.指针和数组 6.二级指针 7.指针数组 1.指针是什么? 指…

动态路由协议解析(rip)

因为今天是1024程序猿节,小刘就在这里祝各位程序猿大佬们节日快乐啦 作者介绍: 💰作者:小刘在C站 ✨每天分享课堂笔记 🌸夕阳下,是最美的绽放 目录 动态路由协议和 rip 1.动态路由 2.动态路由协议工作…

非线性方程求根——牛顿迭代法

一、牛顿法 1.实质:牛顿法实质上是一种线性方法,其基本思想是将非线性方程f(x)0逐步归结为某种线性方程来解。 2.牛顿法公式: 已知方程f(x)0有近似解xk,假设,将f(x)在点xk泰勒展开,有则方程f(x)0可近似表示为&#…

【开关电源六】buck电路的输出纹波定量分析走一波?

在TI官网发现一篇关于降压开关电源的输出纹波定量分析,看完觉得挺不错的,于是大概整理翻译了一下分享出来。以往我们分析开关电源的一次纹波都是定性分析,知道输出电压纹波和电感纹波电流、输出滤波电容ESR等有关,今天我们用数学、…

再看 Logback 源码

三大组件 Logback 构建在三个主要的类上:Logger,Appender 和 Layouts。这三个不同类型的组件一起作用能够让开发者根据消息的类型以及日志的级别来打印日志。 Logger 类作为 logback-classic 模块的一部分。Appender 与 Layouts 接口作为 logback-core 的…

VapSR

1024刚过还得搬砖 注意机制在设计高级超分辨率(SR)网络中起着关键作用。在这项工作中,作者们通过改进注意机制设计了一个高效的SR网络。VapSR以更少的参数优于当前的轻量级网络。董超团队开源超大感受野注意力超分方案 paper:ht…

【数据挖掘 | 可视化】 WordCloud 词云(附详细代码案例)

🤵‍♂️ 个人主页: 计算机魔术师 👨‍💻 作者简介:CSDN内容合伙人,全栈领域优质创作者。 开发环境 编辑器: jupyter notebook 解释器: python 3.7在七夕节中,博主写了一篇为女友收集…

自学Vue之路——Vue介绍及基本语法

今日内容概要 前端发展介绍Vue的快速使用插值语法指令系统之文本指令指令系统之事件指令指令系统之属性指令 前端发展介绍 HTML(5)、CSS(3)、JavaScript(ES5、ES6、ES13):编写一个个的页面 -> 给后端(PHP、Python、Go、Java) -> 后端嵌入模板语法 -> 后端…

Arduino基础知识

目录: 第1章 概述 1.1 Arduino简介 1.2 Arduino内部结构 第2章 Arduino编程 2.1 Arduino开发环境 2.2 Arduino语言概述 2.3 Arduino基本函数 第3章 Arduino通信教程 3.1 SPI通信 3.2 红外通信 3.3 WiFi通信 3.4 蓝牙通信 第4章 Arduino实验 1、接收串…

【学生管理系统】权限管理之用户管理—查询所有用户并关联相关角色

目录 一、查询所有用户(关联角色) 1)后端 2)前端 💟 创作不易,不妨点赞💚评论❤️收藏💙一下 一、查询所有用户(关联角色) 1)后端 修改javaB…

牛客小题练手 | 二叉树(三)

🌈刷题,面试,求职,快来牛客网一起成为offer收割机! 点击注册收割offer 一、BM32 合并二叉树 描述 已知两颗二叉树,将它们合并成一颗二叉树。合并规则是:都存在的结点,就将结点值加…

前端无法渲染CSS文件

🚀 优质资源分享 🚀 学习路线指引(点击解锁)知识定位人群定位🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一…

Air780E模块PPP应用开发指南

目录PPP拨号windows下PPP拨号配置标准调制解调器设置拨号连接验证拨号结果linux下PPP拨号1.安装 PPP 拨号软件2.创建拨号脚本文件/etc/ppp/peers/air-ppp/etc/ppp/peers/air-chat-connect/etc/ppp/peers/air-chat-disconnect3.创建好以上三个文件后就可以通过pppd拨号了4.拨号成…