介绍
在本文中,我们将用Go实现环形缓冲区(Ring Buffer)
Ring Buffer
环形缓冲区(或循环缓冲区)是一种有界循环数据结构,用于在两个或多个线程之间缓冲数据。当我们继续写入环形缓冲区时,它会在到达末尾时回绕。
原理
环形缓冲区是使用在边界处环绕的固定大小数组实现的。
除了数组之外,它还保留了三个关键变量位置:
- 缓冲区中用于插入元素的下一个可用插槽,
- 缓冲区中的下一个未读元素,
- 数组的结尾,即缓冲区环绕到数组开头的点
环形缓冲区如何处理这些要求的机制因实现而异。我们需要知道的第一件事是容量,缓冲区的固定最大大小。接下来,我们将使用两个单调递增的序列:
- 写入序列:从-1开始,在我们插入元素时递增1
- 读取序列:从0开始,随着我们消耗一个元素递增1我们可以使用取模操作将序列映射到数组中的索引:
arrayIndex = sequence % capacity
取模操作在边界周围将序列连接起来,序列中每个值都对应环形缓冲区中的一个槽:
让我们看看我们如何插入一个元素:
buffer[++writeSequence % capacity] = element
我们在插入元素之前预先增加了序列。为了消费一个元素,我们做一个后增量:
element = buffer[readSequence++ % capacity]
在这种情况下,我们对序列执行后增量。使用一个元素并不会将其从缓冲区中删除,它只是保留在数组中直到被覆盖。
缓冲区空与溢出
当我们循环数组时,我们将开始覆盖缓冲区中的数据。如果缓冲区已满,我们可以选择覆盖最旧的数据,无论读取序列是否已使用它或阻止覆盖尚未读取的数据。
如果中间值或旧值(例如,商品价格)可以被覆盖,我们可以覆盖数据而无需等待数据被使用。另一方面,如果必须消耗序列中所有值(例如电子商务交易),我们应该等待(阻塞/忙碌等待),直到缓冲区有可用的插槽。
如果缓冲区的大小等于其容量,则缓冲区已满,其中其大小等于未读元素的数量:
size = (writeSequence - readSequence) + 1
isFull = (size == capacity)
如果写序列落后于读序列,则缓冲区为空:
isEmpty = writeSequence < readSequence
如果缓冲区为空,则缓冲区返回空值。
优点与缺点
环形缓冲区是一种高效的FIFO缓冲区。它使用可以预先分配的固定大小的数组,并允许高效的内存访问模式。所有缓冲区操作都是常数时间O(1),包括消耗一个元素,因为它不需要移动元素。
另一方面,确定环形缓冲区的正确大小至关重要。例如,如果缓冲区过小并且读取速度很慢,则写入操作可能会阻塞很长时间。我们可以使用动态调整大小,但它需要移动数据,我们会错过上面讨论的大部分优势。
Go的实现
在了解环形缓冲区的原理之后,我们来用Go实现这个数据结构。
结构体定义
type T string
type RingBuffer struct {
sync.RWMutex
data []T
capacity int
read int
write int
}
我们定义一个环形缓冲区结构体,其中包含值数组、数组大小,读指针和写指针。同时,它也是并发安全的。
初始化
首先,让我们定义一个使用预定义容量初始化缓冲区的构造函数:
const DefaultCapacity = 8
func NewRingBuffer(cap int) *RingBuffer {
ring := &RingBuffer{}
if cap < 1 {
cap = DefaultCapacity
}
ring.data = make([]T, cap)
ring.capacity = cap
ring.read = 0
ring.write = -1
return ring
}
写
接下来,我们将实现Offer操作,在下一个可用槽处将元素插入缓冲区,并在成功时返回 true。如果缓冲区找不到空槽,则返回false,也就是说,我们不能覆盖未读取的值。
func (r *RingBuffer) Offer(t T) bool {
if !r.IsFull() {
next := r.write + 1
r.data[next%r.capacity] = t
r.write++
return true
}
return false
}
因此,我们正在递增写入序列并计算数组中下一个可用插槽的索引。然后,我们将数据写入缓冲区并存储更新的写入序列。
读
最后,我们将实现获取并删除下一个未读元素的轮询操作。轮询操作不会删除元素,但会增加读取序列。
func (r *RingBuffer) Poll() *T {
if !r.IsEmpty() {
next := r.data[r.read%r.capacity]
r.read++
return &next
}
return nil
}
在这里,我们通过计算数组中的索引来读取当前读取序列的数据。然后,如果缓冲区不为空,我们将递增序列并返回值。