数据结构之环形缓冲器

你有过这样的经历吗?当你打开你的电脑,就因为输入你的密码缺导致出现了一个卡顿。这个卡顿的出现,主要是因为电脑处理器醒来发现它需要开始读取键盘上的迷你处理器。但是,这些信息到底是如何存储在那么小的键盘内存空间里呢?这个就是圆形缓冲器作用的场景。

环形缓冲器是一种特定的队列。所以它也被称作为圆形队列。如果你对于队列不太熟悉的话,你从名字上来看你至少能反映出它是一种直线的队伍(如同大家排队去上卫生间)。这个队列里是先进先出(FIFO)。所以意味着,第一个进入队伍的人,会第一个出队。那么到底环形缓冲器是如何与众不同的呢?

环形缓冲区通常储存数据在一个定长的数组里(array)。因此只要长度确定以后,它通过两个指针分别指向这个队列的队尾和队首的位置,来追踪队伍的情况。只需要根据队伍的新元素的增加和减少,相应的向前移动指针。而无需去动态地去操作这个数组。举个例子,队首被推出了队列,那么其他队伍里的元素都需要向前移动一位。这种操作是比较低效的。所以环形缓冲区避免了这种操作。因为这个环形队列的实现是数组,所以只能通过添加新元素到队首或者队尾。假如需要添加元素到队列的中间,则可能需要使用链表来实现。


那么这两个指针分别是怎么工作的?

头指针(head index) - 生产者指针,不断向队列里插入数据的指针。

尾指针(tail index) - 消费者指针,不断向队列里读取数据的指针。

一般来说,这个队列里面没有任何元素的时候和队列已经满的时候,头指针和尾指针是指向同一个位置的。

当元素被添加到队列里时,头指针向前移动一此。当元素从队列里删除时,尾指针同理。但是尾指针永远都不应该跳过头指针,因为你生产者永远排在消费者前面,或者当队列为空时两个指针指向同一个元素。当指针移动到数组的末尾位置,它将重新跳转回数组的起始位置。并且头指针和写指针之间是线程安全的。假如有多个消费者和生产者公用指针,则需要加锁来保证线程安全。

如何区分缓冲区满或者空?

缓冲区是满、或是空,都有可能出现读指针与写指针指向同一位置。有多种策略用于检测缓冲区是满、或是空。

1. 总是保持一个存储单元为空缓冲区中总是有一个存储单元保持未使用状态。缓冲区最多存入(size-1)。个数据。如果读写指针指向同一位置,则缓冲区为空。如果写指针位于读指针的相邻后一个位置,则缓冲区为满。这种策略的优点是简单、粗暴;缺点是语义上实际可存数据量与缓冲区容量不一致,测试缓冲区是否满需要做取余数计算。

2. 使用数据计数这种策略。不使用显式的写指针,而是保持着缓冲区内存储的数据的计数。因此测试缓冲区是空是满非常简单;对性能影响可以忽略。缺点是读写操作都需要修改这个存储数据计数,对于多线程访问缓冲区需要并发控制。

3. 镜像指示位。缓冲区的长度如果是n,逻辑地址空间则为0至n-1;那么,规定n至2n-1为镜像逻辑地址空间。本策略规定读写指针的地址空间为0至2n-1,其中低半部分对应于常规的逻辑地址空间,高半部分对应于镜像逻辑地址空间。当指针值大于等于2n时,使其折返(wrapped)到ptr-2n。使用一位表示写指针或读指针是否进入了虚拟的镜像存储区:置位表示进入,不置位表示没进入还在基本存储区。

4. 在读写指针的值相同情况下,如果二者的指示位相同,说明缓冲区为空;如果二者的指示位不同,说明缓冲区为满。这种方法优点是测试缓冲区满/空很简单;不需要做取余数操作;读写线程可以分别设计专用算法策略,能实现精致的并发控制。 缺点是读写指针各需要额外的一位作为指示位。

如果缓冲区长度是2的幂,则本方法可以省略镜像指示位。如果读写指针的值相等,则缓冲区为空;如果读写指针相差n,则缓冲区为满,这可以用条件表达式(写指针 == (读指针异或缓冲区长度))来判断。

5. 读/写计数用。两个有符号整型变量分别保存写入、读出缓冲区的数据数量。其差值就是缓冲区中尚未被处理的有效数据的数量。这种方法的优点是读线程、写线程互不干扰;缺点是需要额外两个变量。

6. 使用一位记录最后一次操作是读还是写。读写指针值相等情况下,如果最后一次操作为写入,那么缓冲区是满的;如果最后一次操作为读出,那么缓冲区是空。这种策略的缺点是读写操作共享一个标志位,多线程时需要并发控制。

Reference:

https://en.wikipedia.org/wiki/Circular_buffer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。