架构设计:生产者/消费者模式[4]:双缓冲区

发布时间:2014-10-23 23:23:36
来源:分享查询网

版权声明  本文版权归作者编程随想 所有 转载自:http://blog.csdn.net/program_think/article/details/4085731     “双缓冲区”是一个应用很广的手法。该手法用得最多的地方想必是屏幕绘制相关的领域(主要是为了减少屏幕闪烁)。另外,在设备驱动和工控方面,双缓冲也经常被使用。不过今天要聊的,并不是针对上述的某个具体领域,而是侧重于并发方面的同步/互斥开销。   ★为啥要双缓冲区    记得前几天在介绍队列缓冲区 时,提及了普通队列缓冲区的两个性能问题:“内存分配的开销”和“同步/互斥的开销”(健忘的同学,先回去看看那个帖子 复习一下)。“内存分配的开销”已经在介绍环形缓冲区 的时候解决了,而今天要介绍的双缓冲区,就是冲着同步/互斥的开销来的。   为了防止有人给咱扣上“过度设计”的大帽子,又得来一个事先声明:只有当同步或互斥的开销非常明显的时候,你才应该考虑双缓冲区的使用。否则的话,大伙儿还是老老实实用最基本、最简单的队列缓冲区吧。   ★双缓冲区的原理    前面说了一通废话,现在开始切入正题,说说具体实现。   所谓“双缓冲区”,故名思义就是要有俩缓冲区(简称A和B)。这俩缓冲区,总是一个用于生产者,另一个用于消费者。当俩缓冲区都操作完,再进行一次切换(先前被生产者写入的转为消费者读出,先前消费者读取的转为生产者写入)。由于生产者和消费者不会同时 操作同一个 缓冲区(不发生冲突),所以就不需要在读写每一个 数据单元 的时候都进行同步/互斥操作。顺便提一下,这又一次展现了空间换时间 的优化思路。   但是光有俩缓冲区还不够。为了做到“不冲突”,还得再搞两个互斥锁(简称La和Lb),分别对应俩缓冲区。生产者或消费者如果要操作某个缓冲区,必须先拥有对应的互斥锁。补充一句:要达到“不冲突”的效果,其实可以有多种搞法,今天只是挑一个简单的来聊。   ★双缓冲区的几种状态    为了加深某些同学的理解,再描述一下双缓冲区的几种状态。   ◇俩缓冲区都在使用的状态(并发读写)   大多数情况下,生产者和消费者都处于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有锁La;同样的,消费者拥有锁Lb。由于俩缓冲区都是处于独占状态,因此每次读写缓冲区中的元素(数据单元 )都不需要 再进行加锁、解锁操作。这是节约开销的主要来源。   ◇单个缓冲区空闲的状态   由于两个并发实体的速度会有差异,必然会出现一个缓冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。   在这种情况下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入发呆(Suspend)状态。   ◇缓冲区的切换   接着上面的话题。   过了若干时间,消费者终于把B读完。这时候,消费者也要先释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有La并开始读取A的数据。而由于Lb被消费者释放,所以刚才发呆的生产者会缓过神来(Resume)并拥有Lb,然后生产者继续往B写入数据。   经过上述几个步骤,俩缓冲区完成了对调,变为:生产者写入B,消费者读取A。   ★可能的并发问题    本来单个缓冲区的生产者/消费者问题就已经是教科书的经典问题了,现在搞出俩缓冲区,所以就更加耗费脑细胞了。一不小心,就会搞出些并发的Bug,而且并发的Bug还很难调试和测试(这也就是为啥不要轻易使用该玩意儿的原因)。   ◇死锁的问题   假如把前面介绍的操作步骤调换一下顺序:生产者或消费者在操作完当前的缓冲区之后,先去获取另一个缓冲区的锁,再来释放当前缓冲区的锁。那会咋样捏?   一旦两个并发实体同时 处理完各自缓冲区,然后同时 去获取对方拥有的锁,那就会出现典型的死锁(死锁的详细解释参见“这里 ”)场景。它俩从此陷入万劫不复的境地。   ★应用场景    介绍完并发问题,按照本系列 的惯例,最后再来介绍一下双缓冲区在某些场合的应用。   ◇用于并发线程   在线程方式下,首先要考虑的是缓冲区的类型:到底用队列方式还是环形方式。这方面的选择依据在介绍环形缓冲区 的时候已经阐述过了,此处不再啰嗦(省去不少口水)。   另一个需要注意的是,某些编程语言或者程序库提供了的线程安全的缓冲区(比如JDK 1.5引入的ArrayBlockingQueue )。由于这种缓冲区会自动为每次的读写进行同步/互斥,所以就把双缓冲的优势抵消掉了。因此,大伙儿在进行缓冲区选型的时候要避开这类缓冲区。   ◇用于并发进程   在进程间使用双缓冲,先得考察不同IPC类型的特点。由于今天讨论双缓冲的目的是降低同步/互斥的开销,对于那些已经封装了同步/互斥的IPC类型,就没太大必要再去搞双缓冲了(单凭这条就已经排除了好多种IPC类型)。剩下的IPC类型中,比较适合用双缓冲的主要是:共享内存和文件。非常凑巧,这两个玩意儿的特点和适用范围在环形缓冲区的帖子 里面也已经介绍过了,又可以节省不少口水 :)

返回顶部
查看电脑版