数据结构之环形缓冲器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之环形缓冲器相关的知识,希望对你有一定的参考价值。

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

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

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

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

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

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

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

纯功能(持久)环形缓冲区

【中文标题】纯功能(持久)环形缓冲区【英文标题】:Purely functional (persistent) ring buffer 【发布时间】:2018-10-19 18:36:19 【问题描述】:

我想使用纯函数数据结构实现一个环形缓冲区,并进行以下操作

按索引进行高效随机访问 添加到前面 从后面移除

之所以使用持久化数据结构,是因为我有一个写线程和多个读线程,我想避免读线程阻塞写线程。

这可以通过让读取器线程仅在拍摄快照时持有锁然后使用快照进行处理来轻松完成。

支持这些操作的现有可用数据结构有哪些?

双向链表不能高效地进行索引查找,并且是 O(n) Clojure PersistentVector 基于 Phil Bagwell 理想哈希树,支持 log32N 中的索引访问,并且 subvec 可用于从头开始删除元素。 哈希数组映射的 trie 也可以通过将整数存储为键来使用,但可能效率不高。

在这种情况下还可以使用哪种其他纯函数式数据结构?

【问题讨论】:

你能描述一个用例吗?听起来你们有矛盾的目标。 读者还是要屏蔽作者;您不能冒险在读取期间写入更新会使索引无效。 或更准确地说,现有读取会阻塞写入器,但读取请求不能抢占待处理的写入请求,以避免饥饿。 chepner:该对象是不可变的,并且永远不会就地更新。作者无法修改它,它只能创建一个与旧版本共享某些对象的新版本。读取器和写入器将在更新指向最新版本的指针时简单地锁定。拍摄快照后无需锁定即可使用快照。 为什么需要随机访问?似乎没有它,您可以只使用几个concurrent queues之一。 【参考方案1】:

finger tree(在标准库中为Data.Sequence)是持久随机访问序列的首选。我认为它满足您的标准--随机访问索引是 O(log n)(更具体地说,是索引与边缘距离的日志),其他是 O(1)。我不知道有比这更好的持久性数据结构。

【讨论】:

非常感谢 luqui,所以手指树比 clojure PersistentVector 更好? 稍微阅读后,似乎 clojure 持久向量只是 32 叉树。手指树在两端推和弹方面会更好;随机访问应该具有可比性。 @skyde,Data.Sequence 中的手指树有相当糟糕的常数因子。您最好吃(小)对数成本来访问末端,以换取从更粗的树更好的随机访问。当然,这完全取决于您的使用模式。 @dfeuer 你能否澄清你所说的 Data.Sequence 有相当糟糕的常数因子是什么意思。你的意思是随机访问会比 32 叉树慢吗? Clojure 持久向量不能用作队列,因为 subseq 维护对整个底层向量的引用,因此从后面“弹出”的所有项目都不会被垃圾收集:(

以上是关于数据结构之环形缓冲器的主要内容,如果未能解决你的问题,请参考以下文章

linux内核之Kfifo环形队列

EasyRTMP实现RTMP异步直播推送之环形缓冲区设计

环形缓冲区

环形缓冲器(FIFO)转

纯功能(持久)环形缓冲区

使用无锁队列(环形缓冲区)注意事项