`
saybody
  • 浏览: 907508 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

架构设计:生产者/消费者模式[3]:环形缓冲区

阅读更多

  前一个帖子 提及了队列缓冲区可能存在的性能问题及解决方法:环形缓冲区。今天就专门来描述一下这个话题。
  为了防止有人给咱扣上“过度设计”的大帽子,事先声明一下:只有当存储空间的分配/释放非常频繁 并且确实产生了明显 的影响,你才应该考虑环形缓冲区的使用。否则的话,还是老老实实用最基本、最简单的队列缓冲区 吧。还有一点需要说明一下:本文所提及的“存储空间 ”,不仅包括内存,还可能包括诸如硬盘之类的存储介质。<!-- program-think-->

  ★环形缓冲区 vs 队列缓冲区
  ◇外部接口相似
  在介绍环形缓冲区之前,咱们先来回顾一下普通的队列。普通的队列有一个写入端和一个读出端。队列为空的时候,读出端无法读取数据;当队列满(达到最大尺寸)时,写入端无法写入数据。
  对于使用者来讲,环形缓冲区和队列缓冲区是一样的。它也有一个写入端(用于push)和一个读出端(用于pop),也有缓冲区“满”和“空”的状态。所以,从队列缓冲区切换到环形缓冲区,对于使用者来说能比较平滑地过渡。
  ◇内部结构迥异
  虽然两者的对外接口差不多,但是内部结构和运作机制有很大差别。队列的内部结构此处就不多啰嗦了。重点介绍一下环形缓冲区的内部结构。
  大伙儿可以把环形缓冲区的读出端(以下简称R)和写入端(以下简称W)想象成是两个人在体育场跑道上追逐(R追W)。当R追上W的时候,就是缓冲区为空;当W追上R的时候(W比R多跑一圈),就是缓冲区满。
  为了形象起见,去找来一张图并略作修改,如下:

不见图、请


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

  ★环形缓冲区的实现
  如果你手头已经有现成的环形缓冲区可供使用,并且你对环形缓冲区的内部实现不感兴趣,可以跳过这段。
  ◇数组方式 vs 链表方式
  环形缓冲区的内部实现,即可基于数组(此处的数组,泛指连续存储空间)实现,也可基于链表实现。
  数组在物理存储上是一维的连续线性结构,可以在初始化时,把存储空间一次性 分配好,这是数组方式的优点。但是要使用数组来模拟环,你必须在逻辑上把数组的头和尾相连。在顺序遍历数组时,对尾部元素(最后一个元素)要作一下特殊处理。访问尾部元素的下一个元素时,要重新回到头部元素(第0个元素)。如下图所示:

不见图、请


  使用链表的方式,正好和数组相反。链表省去了头尾相连的特殊处理。但是链表在初始化的时候比较繁琐,而且在有些场合(比如后面提到的跨进程的IPC)不太方便使用。
  ◇读写操作
  环形缓冲区要维护两个索引,分别对应写入端(W)和读取端(R)。写入(push)的时候,先确保环没满,然后把数据复制到W所对应的元素,最后W指向下一个元素;读取(pop)的时候,先确保环没空,然后返回R对应的元素,最后R指向下一个元素。
  ◇判断“空”和“满”
  上述的操作并不复杂,不过有一个小小的麻烦:空环和满环的时候,R和W都指向同一个位置!这样就无法判断到底是“空”还是“满”。大体上有两种方法可以解决该问题。
  办法1:始终保持一个元素不用
  当空环的时候,R和W重叠。当W比R跑得快,追到距离R还有一个元素间隔的时候,就认为环已经满。当环内元素占用的存储空间较大的时候,这种办法显得很土(浪费空间)。
  办法2:维护额外变量
  如果不喜欢上述办法,还可以采用额外的变量来解决。比如可以用一个整数记录当前环中已经保存的元素个数(该整数>=0)。当R和W重叠的时候,通过该变量就可以知道是“空”还是“满”。
  ◇元素的存储
  由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储 类型的数据,而不要存储指针(引用) 类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。

  ★应用场合
  刚才介绍了环形缓冲区内部的实现机制。按照前一个帖子 的惯例,我们来介绍一下在线程和进程方式下的使用。
  如果你所使用的编程语言和开发库中带有现成的、成熟的 环形缓冲区,强烈建议使用现成的库,不要重新制造轮子;确实找不到现成的,才考虑自己实现。如果你纯粹是业余时间练练手,那另当别论。
  ◇用于并发线程
  和线程中的队列缓冲区类似,线程中的环形缓冲区也要考虑线程安全的问题。除非你使用的环形缓冲区的库已经帮你实现了线程安全,否则你还是得自己动手搞定。线程方式下的环形缓冲区用得比较多,相关的网上资料也多,下面就大致介绍几个。
  对于C++的程序员,强烈推荐使用boost 提供的circular_buffer 模板,该模板最开始是在boost 1.35版本中引入的。鉴于boost在C++社区中的地位,大伙儿应该可以放心使用该模板。
  对于C程序员,可以去看看开源项目circbuf ,不过该项目是GPL协议的,不太爽;而且活跃度不太高;而且只有一个开发人员。大伙儿慎用!建议只拿它当参考。
  对于C#程序员,可以参考CodeProject上的一个示例
  ◇用于并发进程
  进程间的环形缓冲区,似乎少有现成的库可用。大伙儿只好自己动手、丰衣足食了。
  适用于进程间环形缓冲的IPC类型,常见的有共享内存 和文件。在这两种方式上进行环形缓冲,通常都采用数组的方式实现。程序事先分配好一个固定长度的存储空间,然后具体的读写操作、判断“空”和“满”、元素存储等细节就可参照前面所说的来进行。
  共享内存方式的性能很好,适用于数据流量很大的场景。但是有些语言(比如Java)对于共享内存不支持。因此,该方式在多语言协同开发的系统中,会有一定的局限性。
  而文件方式在编程语言方面支持很好,几乎所有编程语言都支持操作文件。但它可能会受限于磁盘读写(Disk I/O)的性能。所以文件方式不太适合于快速数据传输;但是对于某些“数据单元 ”很大的场合,文件方式是值得考虑的。
  对于进程间的环形缓冲区,同样要考虑好进程间的同步、互斥等问题,限于篇幅,此处就不细说了。
  下一个帖子,咱们来聊一下双缓冲区的使用


版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想 和本文原始地址:

http://program-think.blogspot.com/2009/04/producer-consumer-pattern-3-circle.html

分享到:
评论

相关推荐

    高性能高稳定性分布式的游戏架构,游戏逻辑运行在Disruptor消费者线程中,其它线程都为辅助线程, 整体为多生产者.zip

    1. 学习Disruptor的原理和使用:理解环形缓冲区、事件处理和多生产者-消费者模型。 2. 探究Java并发编程:如何使用线程池,避免线程安全问题,以及如何利用并发库提高程序性能。 3. 分布式系统基础:了解如何设计高...

    16、常用并发设计模式精讲(1).pdf

    - **环形缓冲区**:使用环形缓冲区代替传统的链表结构,减少了内存分配和垃圾回收的压力。 - **单生产者/多消费者模型**:支持一个生产者和多个消费者,简化了并发控制。 - **事件发布机制**:提供了一种低延迟、高...

    15、CPU缓存架构详解&高性能内存队列Disruptor实战

    它采用“生产者—发布者”和“消费者—订阅者”的模式,使得数据一旦被生产者写入,消费者就可以立即读取,无需等待其他操作完成。这种设计避免了传统的锁和条件变量带来的上下文切换,显著提升了处理速度。 在实战...

    DPDK编程指南

    - **环形缓冲区管理(librte_ring)**:提供了一种高效的环形缓冲区实现方式,适用于多生产者和多消费者的场景。 - **内存池管理(librte_mempool)**:负责高效地分配和回收固定大小的对象,减少了频繁调用系统内存...

    实时数据缓冲.pptx

    3. **缓冲区管理策略**:生产者-消费者模型、锁和信号量机制等都可以用来管理缓冲区,确保并发访问的安全性、数据完整性和实时性。 4. **数据类型的支持**:缓冲区需要能够支持不同类型的数据,如字符串、整数等,...

    Disruptor:一种高性能的、在并发线程间数据交换领域用于替换有界限队列的方案

    Disruptor的核心是其环形缓冲区(Ring Buffer)设计,这是一个固定大小的数组,生产者和消费者通过序列号进行同步,而无需锁或其他同步机制。这种设计消除了传统队列中的锁和条件变量,极大地提高了并发性能。 总结...

    DPDK编程指南精选

    - 环形缓冲区管理(librte_ring):提供了一种无锁、多生产者/多消费者的高效数据交换机制。 - 内存池管理(librte_mempool):内存池管理库为应用程序提供了一种分配和管理对象内存池的方式,以减少内存分配的开销...

    串行io disruptor

    Disruptor的工作原理大致如下:它使用环形缓冲区(Ring Buffer)作为基础数据结构,将生产者和消费者的操作解耦,并通过序列号(Sequence)来确保数据的一致性。生产者可以连续写入而不必等待消费者的读取,而消费者...

    开源项目-rodzzlessa24-smq.zip

    对于一个仅有55行代码的消息队列,其设计和实现可能会依赖于一些高效的数据结构和算法,比如链表或者环形缓冲区,来快速地插入和取出消息。此外,为了保持代码的简洁性,它可能没有提供高级功能,如事务支持、优先级...

    13、线程池ForkJoinPool实战及其工作原理分析(1).pdf

    - **生产者-消费者模式**: 解决了多线程之间的同步和通信问题。 - **读写锁模式**: 支持多个线程同时读取共享资源,但只允许一个线程写入。 - **职责链模式**: 在多线程环境中用于处理请求的传递和处理。 通过以上...

    大厂 Go 工程师面试题集锦.docx

    - **知识点**:熟悉生产者消费者模型的工作机制及其在并发编程中的应用。 - **解释**:生产者负责生成数据放入缓冲区,消费者负责从中取出数据处理,适用于多线程环境下资源共享。 6. **反转链表算法** - **...

    大数据面试题,找工作前必刷!

    - **环形模式**:每个Agent既可以是生产者也可以是消费者,形成闭环。 **7. flume 不采集 Nginx 日志,通过 Logger4j 采集日志,优缺点是什么?** - **优点**: - **灵活性**:可以针对不同应用程序配置不同的...

    dpdk记录文档

    每个CPU核心都有自己的本地缓存队列,当本地队列中的内存块不足时,会从环形缓冲区中获取新的内存块。 5. **网络报文缓存块管理组件** - 提供了用于存储网络数据包信息的缓存块的创建和释放接口。缓存块存储在内存...

    立式环形

    环形队列是一种特殊的线性数据结构,它的首尾相连形成一个环状,常用于实现高效的入队和出队操作,例如在并发编程中处理生产者消费者问题。环形缓冲区则常用于在网络通信或者多媒体数据流处理中,它可以有效地管理...

    北邮2016年计算机学科基础综合803考研真题参考答案

    生产者和消费者通过P-V操作来同步访问缓冲区,确保不会发生冲突。 --- **43. 题目分析:** 题目考查的是指令集架构的设计。 **答案解析:** 题目描述了一个简单的指令格式设计问题。根据题目描述,寄存器Rd和Rs各...

    Real-time Big Data Analytics 无水印pdf 0分

    如LMAX架构中的环形缓冲区是Disruptor的核心,能够实现极低延迟的消息处理。通过优化生产者和消费者的设计,以及利用ZeroMQ和Netty等高性能通信框架,可以显著提高数据处理的效率。 #### 12. 大数据技术平台 大数据...

    mpmc

    在多线程环境下,多个生产者线程可以同时向队列添加元素,而多个消费者线程也可以同时从中取出元素,这种设计模式提高了系统的并行处理能力。 在Java中,JDK的Concurrent包并没有提供原生的MPMC队列实现。然而,有...

Global site tag (gtag.js) - Google Analytics