标签:
作者:曾志优
出处: http://www.cnblogs.com/zengzy
1、环形缓冲区
缓冲区的好处,就是空间换时间和协调快慢线程。缓冲区可以用很多设计法,这里说一下环形缓冲区的几种设计方案,可以看成是几种环形缓冲区的模式。设 计环形缓冲区涉及到几个点,一是超出缓冲区大小的的索引如何处理,二是如何表示缓冲区满和缓冲区空,三是如何入队、出队,四是缓冲区中数据长度如何计算。
ps.规定以下所有方案,在缓冲区满时不可再写入数据,缓冲区空时不能读数据
设缓冲区大小为N,队头out,队尾in,out、in均是下标表示:
同样假设缓冲区大小为N,队头out,队尾in,out、in为数组下标,但数据类型为unsigned int。
这个改进的思想来自linux内核循环队列kfifo,这里解释一下几个行为的含义及原理
⑴上调缓冲区大小至2的幂
这是方便取模,x%M == x&(M-1) 为真,位运算的效率比取模要高。用一个例子来分析一下为什么等式成立的:
假设M=8=2³,那么M-1=7,二进制为0000 0111
①若 x<8 ----> x&7=x , x%8 = x,等式成立
②若 x>8 ----> x = 2^a+2^b+2^c+... 比如,51 = 1+2+16+32 = 2^0+2^1+2^4+2^5 ,求 51&7时,由于7的二进制0000 0111,所以2的幂只要大于等于2³的数,与上7结果都是0,所以2^4 & 7 = 0 , 2^5 & 7 = 0, (2^0+2^1+2^4+2^5) & (7) = 2^0+2^1=3。而根据①,(2^0+2^1)&7 = (2^0+2^1)%8 ,所以51&7=51%8
综上得证。
⑵out、in类型设计为unsigned int
无符号整形的溢出之后,又从0开始计数:MAX_UNSIGNED_INT + 1 = 0 ,MAX_UNSIGNED_INT + 2 = 1 ,... 。
in、out溢出之前,都能通过&把in、out映射到正确的位置上,那溢出之后呢?可以举个例子来:
假设现在in=MAX_UNSIGNED_INT,那么in & (M-1) = M-1 ,也就是最后一个位置,再入队时,应该从头开始入队,也就是0,而in+1也为0,所以即使溢出了,(in+1)&(M-1)仍然能映射到正确的 位置。这就是为什么我们入队出队只要做个与映射和++操作就能保证正确的原因。
而,根据入队和出队的操作,队列中的元素总是维持在[out,in)这个区间中,由于溢出可能存在,这个区间有三种情况:
根据上面三种情况,in-out总是表示环形队列中数据的长度
不得不惊叹,linux内核中的kfifo实现实在是太精妙了。相比前面的版本,所有的取余操作都改成了与运算,入队出队,求缓冲区数据长度都变得非常简单。
环形缓冲区的链表实现比数组实现要简单一些,可以用下图的这种设计方案:
假设要求环形缓冲区大小为N
当然,链表结点的设定是自由的,链表结点本身可以内含数组、链表、哈希表等等,例如下面这样,内含一个数组
这时,可以增设两个变量out_pos,in_pos。假设结点内数组的大小为N_ELEMS,整个链表结点的数量为node_nums
上面链表环形队列出队列可能释放内存,入队列可能申请内存,所以,可以用个空闲链表把该释放的内存管理起来,入队列时,如果要增加结点,先从空闲链表中取结点,取不到再去申请内存,这样就可以避免多次分配释放内存了,至于其他的操作都是一样的。
上边只是简单的说了下入队出队等操作,事实上,缓冲区往往是和读写线程伴随出现 的,缓冲区中的每一个资源,对于同类线程可能需要互斥访问,也可能可以共享使用,而不同类线程间(读写线程)往往需要做同步操作。比如,读线程之间可能共 享缓冲区的每一个资源,也可能互斥使用每个资源,通常,在缓冲区满时写线程不能写,缓冲区空时读线程不能读,也就是读写线程要求同步。这其实就是操作系统 课程上PV操作的几个经典模式,如果读读之间、写写之间要求互斥使用资源,并且读写线程间不要求互斥,就是生产者消费者问题,如果读读之间不要求互斥(每 个资源可供多个读线程共同使用),写写之间要求互斥(每个资源仅供一个写线程使用),并且读写线程也要求互斥(读的时候不能写,写的时候不能读),就是读 写者问题。
下面会以生产者消费者模式和1.2节改进版的循环缓冲区为例,来说说并发循环队列有锁实现,下一篇说无锁实现。关于读写者的问题,以后有时间再详谈。
先提一嘴生产者消费者的优点吧
现在正式开工,根据生产者和消费者的数量,可以把生产者消费者划分为四种类型,1:1,1:N,M:1,M:N。
然后再做个规定,规定环形缓冲区的大小为M,M为2的幂次方,in、out统一称为slot。
一个生产者,一个消费者,缓冲区可用资源数为M。
这种情况只要同步生产者和消费者,同步的方法是用两个信号量available_in_slots,available_out_slots分别表示生产者有多个可用资源、消费者有多个可用资源, 每生产一个产品,生产者可用资源减1,消费者可用资源加1,这点可用PV操作来实现,用P操作可以消耗1个资源,P操作结束资源数减1,V操作可以生产1 个资源,V操作结束后资源数加1。初始时,available_in_slots=M,表示生产者有M个空间可放产 品,available_out_slots=0,表示消费者还没有可用资源:
available_in_slots = M; available_out_slots = 0; in=out=0; void producer() { while(true){ P(available_in_slots); queue[(in++)&(M-1)] = data; V(available_out_slots) } } void consumer() { while(true){ P(available_out_slots); queue[(out++)&(M-1)] = data; V(available_in_slots) } }
一个生产者,多个消费者,缓冲区可用资源数位M。
这种情况下,消费者有多个,消费者之间对out slot要互斥访问,用out_slot_mutex来实现消费者间的互斥,拿到out_slot_mutex的消费者线程才得以继续执行,没拿到的只能 阻塞。生产者消费者要同步,用available_in_slots,available_out_slots来实现生产者消费者的同步。
详细内容看原文连接。
标签:
原文地址:http://www.cnblogs.com/rechen/p/5143841.html