Go 实现 ringbuffer

Go语言精选

共 3717字,需浏览 8分钟

 ·

2021-09-13 20:15

文章目录

  • 计数法实现

  • 镜像指示位实现

ringbuffer,是环形缓存区, 或叫环形队列。

不同于一般常见的队列,环形队列首尾相连,通过移动指针来控制队列中内容的读写。

这样做有什么好处呢?

最大的好处是环形队列出队(读取)后,不需要对后续队列内容进行搬移,可以后续由入队(写入)覆盖。

下面来看下一种常见的实现方式, 通过读写指针计数来实现环形队列

计数法实现

先过一遍代码,后边会有图解版说明,方便理解

代码参考自鸟窝无限缓存 chanx[1]

数据结构如下:

type T interface{}
type RingBuffer struct {
 buf         []T // 队列
 initialSize int // 初始容量
 size        int // 当前容量
 r           int // 读指针计数
 w           int // 写指针计数
}

读取时:

// 读写指针相遇为空
func (r *RingBuffer) IsEmpty() bool {
 return r.r == r.w
}

func (r *RingBuffer) Read() (T, error) {
 if r.IsEmpty() {
  return nil, ErrIsEmpty
 }

 // 读取指针处获取,并向后移动一位
 v := r.buf[r.r]
 r.r++
 // 绕一圈后重置归零
 if r.r == r.size {
  r.r = 0
 }

 return v, nil
}

写入时:

func (r *RingBuffer) Write(v T) {
 // 从写指针处获取,并加一偏移
 r.buf[r.w] = v
 r.w++
 // 绕一圈后重置归零
 if r.w == r.size {
  r.w = 0
 }

 // 当写入后,写指针遇到读指针,即队列已写满
 if r.w == r.r {
  // 自动扩容
  r.grow()
 }
}

具体扩容:

func (r *RingBuffer) grow() {
 var size int
 if r.size < 1024 {
  // 2倍增长
  size = r.size * 2
 } else {
  // 1.25倍增长
  size = r.size + r.size/4
 }

 buf := make([]T, size)

 // 拷贝读指针 后 待读取内容
 copy(buf[0:], r.buf[r.r:])
 // 拷贝读指针 前 待读取内容
 copy(buf[r.size-r.r:], r.buf[0:r.r])

 // 拷贝后读指针为头部
 r.r = 0
 // 写指针为尾部(原队列大小)
 r.w = r.size
 r.size = size
 r.buf = buf
}

为方便理解,具体举例如下图所示,对照看应该很容易理解:

ringbuffer

除此外,还有一种镜像指示位方法实现环形队列

镜像指示位实现

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

大意就是在计数可以允许绕两圈(2n-1),假定写指针一圈后和读指针相遇

原来绕一圈归零后相遇(r == w)为队列已满。

现在绕一圈后(未归零)相遇(w == (r + n))为队列已满。

如下图:

mirror solution

而且如果队列长度刚好是 2 的幂,则可以利用位运算来判断差值是否为n

func (r *RingBuffer) IsFull() bool {
 // r.r > size: r.r-size
 // r.r < size: r.r+size
 return (r.r ^ r.size) == r.w
}

然后读写指针的位置获取和偏移也可以按位与来替代取模运算

// 位置
v := r.buf[r.r&(r.size-1)]
r.buf[r.w&(r.size-1)] = v
// 偏移
func (r *RingBuffer) incrCursor(p *int) {
 *p = (*p + 1) & (2*r.size - 1)
}

代码详见ringbuffer-mirror[3], 感兴趣可以去尝试下。

参考资料

[1]

鸟窝无限缓存chanx: https://github.com/smallnest/chanx/blob/main/ringbuffer.go

[2]

wiki: https://zh.wikipedia.org/wiki/%E7%92%B0%E5%BD%A2%E7%B7%A9%E8%A1%9D%E5%8D%80#%E9%95%9C%E5%83%8F%E6%8C%87%E7%A4%BA%E4%BD%8D

[3]

ringbuffer-mirror: https://github.com/NewbMiao/algorithm-go/tree/master/ringbuffer/virtualmemory



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


浏览 82
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报