经过前面两个帖子的铺垫,今天终于开始聊一些具体的编程技术了。由于不同的缓冲区类型、不同的并发场景对于具体的技术实现有较大的影响。为了深入浅出、便于大伙儿理解,咱们先来介绍最传统、最常见的方式。也就是单个生产者对应单个消费者,当中用队列
(FIFO)作缓冲。<!-- program-think-->
关于并发的场景,在之前的帖子“进程还线程?是一个问题!
”中,已经专门论述了进程和线程各自的优缺点,两者皆不可偏废。所以,后面对各种缓冲区类型的介绍都会同时提及进程方式和线程方式。
★线程方式
先来说一下并发线程中使用队列的例子,以及相关的优缺点。
◇内存分配的性能
在线程方式下,生产者和消费者各自是一个线程。生产者把数据写入队列头(以下简称push),消费者从队列尾部读出数据(以下简称pop)。当队列为空,消费者就稍息(稍事休息);当队列满(达到最大长度),生产者就稍息。整个流程并不复杂。
那么,上述过程会有什么问题捏?一个主要的问题是关于内存分配的性能开销。对于常见的队列实现:在每次push时,可能涉及到堆内存
的分配;在每次pop时,可能涉及堆内存
的释放。假如生产者和消费者都很勤快,频繁地push、pop,那内存分配的开销就很可观了。对于内存分配的开销,用Java的同学可以参见前几天的帖子“Java性能优化[1]
”;对于用C/C++的同学,想必对OS底层机制会更清楚,应该知道分配堆内存
(new或malloc)会有加锁的开销和用户态/核心态切换
的开销。
那该怎么办捏?请听下文分解,关于“生产者/消费者模式[3]:环形缓冲区
”。
◇同步和互斥的性能
另外,由于两个线程共用一个队列,自然就会涉及到线程间诸如同步啊、互斥啊、死锁啊等等劳心费神的事情。好在"操作系统"这门课程对此有详细介绍,学过的同学应该还有点印象吧?对于没学过这门课的同学,也不必难过,网上相关的介绍挺多的(比如"这里
"),大伙自己去瞅一瞅。关于这方面的细节,咱今天就不多啰嗦了。
这会儿要细谈的是,同步和互斥的性能开销。在很多场合中,诸如信号量、互斥量等玩意儿的使用也是有不小的开销的(某些情况下,也可能导致用户态/核心态切换)。如果像刚才所说,生产者和消费者都很勤快,那这些开销也不容小觑啊。
这又该咋办捏?请听下文的下文分解,关于“生产者/消费者模式[4]:双缓冲区
”。
◇适用于队列的场合
刚才尽批判了队列的缺点,难道队列方式就一无是处?非也。由于队列是很常见的数据结构,大部分编程语言都内置了队列的支持(具体介绍见"这里
"),有些语言甚至提供了线程安全的队列(比如JDK 1.5引入的ArrayBlockingQueue
)。因此,开发人员可以捡现成,避免了重新发明轮子。
所以,假如你的数据流量不是很大,采用队列缓冲区的好处还是很明显的:逻辑清晰、代码简单、维护方便。比较符合KISS原则。
★进程方式
说完了线程的方式,再来介绍基于进程的并发。
跨进程的生产者/消费者模式,非常依赖于具体的进程间通讯(IPC)方式。而IPC的种类名目繁多,不便于挨个列举(毕竟口水有限)。因此咱们挑选几种跨平台、且编程语言支持较多的IPC方式来说事儿。
◇匿名管道
感觉管道是最像队列的IPC类型。生产者进程在管道的写端
放入数据;消费者进程在管道的读端取出数据。整个的效果和线程中使用队列非常类似,区别在于使用管道就无需操心线程安全、内存分配等琐事(操作系统暗中都帮你搞定了)。
管道又分命名管道
和匿名管道
两种,今天主要聊匿名管道。因为命名管道在不同的操作系统下差异较大(比如Win32和POSIX,在命名管道的API接口和功能实现上都有较大差异;有些平台不支持命名管道,比如Windows
CE)。除了操作系统的问题,对于有些编程语言(比如Java)来说,命名管道是无法使用的。所以我一般不推荐使用这玩意儿。
其实匿名管道在不同平台上的API接口,也是有差异的(比如Win32的CreatePipe和POSIX的pipe,用法就很不一样)。但是我们可以仅使用标准输入和标准输出(以下简称stdio)来进行数据的流入流出。然后利用shell的管道符把生产者进程和消费者进程关联起来(没听说过这种手法的同学,可以看"这里
")。实际上,很多操作系统(尤其是POSIX风格的)自带的命令都充分利用了这个特性来实现数据的传输(比如more、grep等)。
这么干有几个好处:
1、基本上所有操作系统都支持在shell方式下使用管道符。因此很容易实现跨平台。
2、大部分编程语言都能够操作stdio,因此跨编程语言也就容易实现。
3、刚才已经提到,管道方式省却了线程安全方面的琐事。有利于降低开发、调试成本。
当然,这种方式也有自身的缺点:
1、生产者进程和消费者进程必须得在同一台主机上,无法跨机器通讯。这个缺点比较明显。
2、在一对一的情况下,这种方式挺合用。但如果要扩展到一对多或者多对一,那就有点棘手了。所以这种方式的扩展性要打个折扣。假如今后要考虑类似的扩展,这个缺点就比较明显。
3、由于管道是shell创建的,对于两边的进程不可见(程序看到的只是stdio)。在某些情况下,导致程序不便于对管道进行操纵(比如调整管道缓冲区尺寸)。这个缺点不太明显。
4、最后,这种方式只能单向传数据。好在大多数情况下,消费者进程不需要传数据给生产者进程。万一你确实需要信息反馈(从消费者到生产者),那就费劲了。可能得考虑换种IPC方式。
顺便补充几个注意事项,大伙儿留意一下:
1、对stdio进行读写操作是以阻塞方式进行。比如管道中没有数据,消费者进程的读操作就会一直停在哪儿,直到管道中重新有数据。
2、由于stdio内部带有自己的缓冲区(这缓冲区和管道缓冲区是两码事),有时会导致一些不太爽的现象(比如生产者进程输出了数据,但消费者进程没有立即
读到)。具体的细节,大伙儿可以看"这里
"。
◇SOCKET(TCP方式)
基于TCP方式的SOCKET通讯是又一个类似于队列的IPC方式。它同样保证了数据的顺序到达;同样有缓冲的机制。而且这玩意儿也是跨平台和跨语言的,和刚才介绍的shell管道符方式类似。
SOCKET相比shell管道符的方式,有啥优点捏?主要有如下几个优点:
1、SOCKET方式可以跨机器(便于实现分布式)。这是主要优点。
2、SOCKET方式便于将来扩展成为多对一或者一对多。这也是主要优点。
3、SOCKET可以设置阻塞和非阻塞方法,用起来比较灵活。这是次要优点。
4、SOCKET支持双向通讯,有利于消费者反馈信息。
当然有利就有弊。相对于上述shell管道的方式,使用SOCKET在编程上会更复杂一些。好在前人已经做了大量的工作,搞出很多SOCKET通讯库和框架给大伙儿用(比如C++的ACE
库、Python的Twisted
)。借助于这些第三方的库和框架,SOCKET方式用起来还是比较爽的。由于具体的网络通讯库该怎么用不是本系列的重点,此处就不细说了。
虽然TCP在很多方面比UDP可靠,但鉴于跨机器通讯先天的不可预料性(比如网线可能被某傻X给拔错了,网络的忙闲波动可能很大),在程序设计上我们还是要多留一手。具体该如何做捏?可以在生产者进程
和消费者进程
内部各自再引入基于线程
的"生产者/消费者模式"。这话听着像绕口令,为了便于理解,画张图给大伙儿瞅一瞅。

这么做的关键点在于把代码分为两部分:生产线程和消费线程属于和业务逻辑相关的代码(和通讯逻辑无关);发送线程和接收线程属于通讯相关的代码(和业务逻辑无关)。
这样的好处是很明显的,具体如下:
1、能够应对暂时性
的网络故障。并且在网络故障解除后,能够继续工作。
2、网络故障的应对处理方式(比如断开后的尝试重连),只影响发送和接收线程,不会影响生产线程和消费线程(业务逻辑部分)。
3、具体的SOCKET方式(阻塞和非阻塞)只影响发送和接收线程,不影响生产线程和消费线程(业务逻辑部分)。
4、不依赖TCP自身的发送缓冲区和接收缓冲区。(默认的TCP缓冲区的大小可能无法满足实际要求)
5、业务逻辑的变化(比如业务需求变更)不影响发送线程和接收线程。
针对上述的最后一条,再多啰嗦几句。如果整个业务系统中有多个进程是采用上述的模式,那或许可以重构一把:在业务逻辑代码和通讯逻辑代码之间切一刀,把业务逻辑无关的部分封装成一个通讯中间件(说中间件
显得比较牛X :-)。如果大伙儿对这玩意儿有兴趣,以后专门开个帖子聊。
下一个帖子
,咱们来介绍一下环形缓冲区的话题。
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想
和本文原始地址:
http://program-think.blogspot.com/2009/03/producer-consumer-pattern-2-queue.html
分享到:
相关推荐
【生产者/消费者模式】是一种常见的并发编程和系统设计模式,它主要解决的是在多线程环境下,如何协调生产者和消费者之间的数据处理问题。在软件开发中,生产者通常是生成数据的一方,而消费者则是处理这些数据的...
而在采用生产者消费者模式之后,生产者和消费者只需要关心自己的任务即可,它们之间通过缓冲区进行交互,这样极大地降低了双方的耦合性,从而使得系统更加灵活,易于维护和扩展。 其次,生产者消费者模式支持并发。...
### 基于队列的状态机—生产者消费者架构知识点详解 #### 一、状态机的概念与作用 **状态机**是一种广泛应用于程序设计中的模式,尤其在LabVIEW这类图形化编程环境中,状态机被用来控制程序流程和状态转换。在...
在计算机科学领域,生产者与消费者模型是一种经典的同步机制,被广泛应用于多线程编程、操作系统设计以及分布式系统架构中。该模型主要解决的是多个进程或线程之间如何有效地共享资源的问题。在本次实验报告中,我们...
在“LabView图形化编程语言之生产者消费者架构串口数据高速采集”这个主题中,我们将深入探讨如何使用LabView来构建高效的数据采集系统,特别是涉及到串口通信和生产者消费者模式。 首先,让我们理解生产者消费者...
消息队列作为两者之间的缓冲区,确保了生产者和消费者可以独立工作,避免了因消费者处理速度慢于生产者生成数据速度而引发的问题,如数据丢失。 在LabVIEW中,实现生产者消费者架构通常会用到以下几个关键元素: 1....
这种模式遵循一个基本原理:生产者负责生成数据,而消费者负责消费这些数据,两者之间通过一个共享的数据缓冲区进行通信。 在Java中实现生产者消费者模式,主要依赖于Java提供的并发工具类,如`BlockingQueue`接口...
这种模型常通过队列实现,队列作为一个缓冲区,存储生产者产生的数据供消费者消费。 初级技术教程可能从基础概念和线程的基本操作开始,例如线程的创建、启动、停止和同步。这部分可能会讲解如何使用Java的Thread类...
生产者消费者问题是一个经典的多线程同步问题,来源于操作系统理论,用于模拟两个或多个相互依赖的进程或线程之间的协作。在这个场景下,“生产者”是生成数据的实体,而“消费者”则负责处理这些数据。这个问题的...
在操作系统领域,生产者-消费者进程同步问题是一个经典的问题,它体现了多进程间协作与通信的基本原理。...在实际应用中,这种模型可以扩展到多个生产者和消费者,或者更复杂的系统架构,例如消息队列、缓冲池等。
- **特点**:生产者将数据放入队列或缓冲区,消费者从中取出数据进行处理。 - **优势**:简单且易于实现。 - **应用场景**:适合于数据量不大、处理逻辑简单的场景。 ##### 3.2 Producer/Consumer(Event) (基于事件...
该问题通常涉及一组生产者进程(负责生成数据)和一组消费者进程(负责处理数据),它们共享一个固定大小的缓冲区。生产者将数据放入缓冲区,而消费者从中取出数据。 #### 信号量解决方案 信号量是一种广泛使用的...
5. **缓冲区管理**:在实验设计文档中,可能会详细描述如何使用数据结构(如队列)来表示缓冲区,以及如何在生产者和消费者之间安全地传递数据。生产者会将产品放入缓冲区,消费者则从中取出产品。 6. **事件和消息...
首先,我们需要创建一个队列作为数据缓冲区,它在生产者和消费者之间起到中介的作用。生产者将串口接收到的数据放入队列,而消费者从队列中取出数据进行处理。这样,即使消费者处理速度较慢,生产者也可以继续接收...
消息队列(Message Queue)是分布式系统中的重要组件,它作为数据缓冲区,允许生产者发送消息而不必立即等待消费者的响应。通过这种方式,生产者和消费者可以独立工作,提高了系统的可扩展性和灵活性。 二、...
2. **TaskQueue**:任务队列,作为生产者和消费者的缓冲区,存储待处理的任务。它通常实现为有界的并发队列,以保证系统的稳定性和防止资源耗尽。 3. **Executor**:执行器,负责真正执行任务。它可以是单线程、多...
实验程序的结构图(流程图)展示了整个系统的架构和流程,包括生产者线程和消费者线程之间的交互、信号量的使用和释放、缓冲区的读取和写入等。 数据结构及信号量定义的说明中,CSemaphore 类的对象保存了对当前...
消息队列是一种中间件,它的主要任务是作为生产者和消费者之间的缓冲区。生产者是产生数据或事件的组件,而消费者则是处理这些数据的组件。在聊天室场景中,用户发送的消息就是生产者产生的数据,而接收并显示消息的...
4. **队列**:存储消息的缓冲区,遵循先进先出(FIFO)原则,即消息按照被放入队列的顺序被取出。 5. **主题**(Topic):在发布/订阅模型中,消息队列可以有多个主题,每个主题可以有多个订阅者。生产者发布消息到...
- **Message(消息)**: 消息是队列中的基本单元,它由生产者创建并发送到队列,然后由消费者消费。 - **Producer(生产者)**: 生产者是创建和发送消息的应用程序。 - **Queue(队列)**: 存储消息的缓冲区,...