日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

詳解Golang如何實現一個環形緩沖器_Golang

作者:jiaxwu ? 更新時間: 2022-10-30 編程語言

背景

環形緩沖器(ringr buffer)是一種用于表示一個固定尺寸、頭尾相連的緩沖區的數據結構,適合緩存數據流。

在使用上,它就是一個固定長度的FIFO隊列:

在邏輯上,我們可以把它當成是一個環,上面有兩個指針代表當前寫索引和讀索引:

在實現上,我們一般是使用一個數組去實現這個環,當索引到達數組尾部的時候,則重新設置為頭部:

kfifo實現

kfifo是Linux內核的隊列實現,它具有以下特性:

  • 固定長度:長度是固定的,而且是向上取最小的2的平方,主要是為了實現快速取余。
  • 無鎖:在單生產者和單消費者的情況下,是不需要加鎖的。主要是因為索引in和out是不回退的,一直往前。
  • 快速取余:我們都直到到達隊列末尾的時候,索引需要回退到開頭。最簡單的實現方式就是對索引取余,比如索引in現在是8,隊列長度是8,in%len(q)即可回退到開頭,但是取余操作%還是比較耗時的,因此kfifo使用in&mask實現快速取余,其中mask=len(q)-1

無鎖

上面我們說到,這個無鎖是有條件的,也就是必須在單生產者單消費者情況下。這種情況下,同一時刻最多只可能會有一個寫操作和一個讀操作。但是在某一個讀操作(或寫操作)的期間,可能會有多個寫操作(或讀操作)發生。

因為索引in和out是不回退的,因此in一直會在out前面(或者重合)。而且in只被寫操作修改,out只被讀操作修改,因此不會沖突。

這里可能有人會擔心索引溢出的問題,比如in到達math.MaxUint64,再+1則回到0。但是其實并不影響in和out之間的距離:

package main

import (
	"fmt"
	"math"
)

func main() {
	var in uint = math.MaxUint64
	var out uint = math.MaxUint64 - 1
	fmt.Println(in - out) // 1
	in++
	fmt.Println(in - out) // 2
	out++
	fmt.Println(in - out) // 1
}

當然如果連續兩次溢出,就會出現問題。但是由于數組長度是int類型,因此也沒辦法超過math.MaxUint64,也就是in和out之間的距離最多也就是2^62,因為math.MaxInt642^63-1,沒辦法向上取2的平方了。因此也不會出現溢出兩倍math.MaxUint64的情況,早在溢出之前就隊列滿了。

快速取余

前面提到取余是通過in&mask實現的,這有一個前提條件,也就是長度必須是2的次方,因此在創建數組的時候,長度會向上取最小的2的平方。例如一個長度為8的kfifo,在二進制表示下:

len  = 0000 1000 // 十進制8,隊列長度
mask = 0000 0111 // 十進制7,掩碼

in   = 0000 0000 // 十進制0,寫索引
in & mask => 0000 0000 // 十進制0,使用 & mask
in % len  => 0000 0000 // 十進制0,使用 % len

in         = 0000 0001 // 十進制1,寫索引
in & mask => 0000 0001 // 十進制1,使用 & mask
in % len  => 0000 0001 // 十進制1,使用 % len

in         = 0000 0001 // 十進制1,寫索引
in & mask => 0000 0001 // 十進制1,使用 & mask
in % len  => 0000 0001 // 十進制1,使用 % len

in         = 0000 1000 // 十進制8,寫索引
in & mask => 0000 0000 // 十進制0,使用 & mask
in % len  => 0000 0000 // 十進制0,使用 % len

in         = 0001 0001 // 十進制17,寫索引
in & mask => 0000 0001 // 十進制1,使用 & mask
in % len  => 0000 0001 // 十進制1,使用 % len

可以看到,使用& mask的效果是和% len一樣的。

然后我們做一個簡單的性能測試:

package main

import "testing"

var (
	Len  = 8
	Mask = Len - 1
	In   = 8 - 5
)

// % len
func BenchmarkModLen(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = In % Len
	}
}

// & Mask
func BenchmarkAndMask(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = In & Mask
	}
}

測試結果:

BenchmarkModLen-8 ? ? ? 1000000000 ? ? ? ? ? ? ? 0.3434 ns/op
BenchmarkAndMask-8 ? ? ?1000000000 ? ? ? ? ? ? ? 0.2520 ns/op

可以看到& mask性能確實比% len好很多,這也就是為什么要用& Mask來實現取余的原因了。

數據結構

數據結構和上面介紹的一樣,in、out標識當前讀寫的位置;mask是size-1,用于取索引,比%size更加高效;

type Ring[T any] struct {
	in   uint64 // 寫索引
	out  uint64 // 讀索引
	mask uint64 // 掩碼,用于取索引,代替%size
	size uint64 // 長度
	data []T    // 數據
}

Push()

Push()操作很簡單,首先r.in & r.mask得到寫索引,讓寫索引前進一格,然后存入數據。

// 插入元素到隊尾
func (r *Ring[T]) Push(e T) {
	if r.Full() {
		panic("ring full")
	}
	in := r.in & r.mask
	r.in++
	r.data[in] = e
}

Pop()

Pop()操作同理,根據r.out & r.mask得到讀索引,讓讀索引前進一格,然后讀取數據。

// 彈出隊頭元素
func (r *Ring[T]) Pop() T {
	if r.Empty() {
		panic("ring emtpy")
	}
	out := r.out & r.mask
	r.out++
	return r.data[out]
}

性能測試

Round實現是使用& mask,同時長度會向上取2的平方;Fix實現是使用% size保持參數的長度。

測試代碼是不斷的Push()然后Pop():

func BenchmarkRoundPushPop(b *testing.B) {
	for i := 0; i < b.N; i++ {
		r := New[int](RoundFixSize)
		for j := 0; j < RoundFixSize; j++ {
			r.Push(j)
		}
		for j := 0; j < RoundFixSize; j++ {
			r.Pop()
		}
	}
}

測試結果:& mask的性能明顯好于% size

BenchmarkRoundPushPop-8 ? ? ? ? ? ? 2544 ? ? ? ? ? ?405621 ns/op // & mask
BenchmarkFixPushPop-8 ? ? ? ? ? ? ? ?678 ? ? ? ? ? 1740489 ns/op // % size

無界環形緩沖器

我們可以在寫數據的時候判斷是否空間已滿,如果已滿我們可以進行動態擴容,從而實現一個無界環形緩沖器。

Push()

在Push()時檢查到空間滿時,調用grow()擴展空間即可:

// 插入元素到隊尾
func (r *Ring[T]) Push(e T) {
	if r.Full() {
                // 擴展空間
		r.Grow(r.Cap() + 1)
	}
	in := r.in % r.size
	r.in++
	r.data[in] = e
}

grow()

擴容一般是擴展為當前容量的兩倍,然后把原來數據copy()到新的數組,更新字段即可:

// 擴容
func (r *Ring[T]) Grow(minSize uint64) {
	size := mmath.Max(r.size*2, minSize)
	if size > MaxSize {
		panic("size is too large")
	}
	if size < 2 {
		size = 2
	}
	// 還沒容量,直接申請,因為不需要遷移元素
	if r.size == 0 {
		r.data = make([]T, size)
		r.size = size
		return
	}
	data := make([]T, size)
	out := r.out % r.size
	len := r.Len()
	copied := copy(data[:len], r.data[out:])
	copy(data[copied:len], r.data)
	r.out = 0
	r.in = len
	r.size = size
	r.data = data
}

線程安全性

由于可能會動態擴容,需要修改out、in指針,因此需要加鎖保證安全。

代碼地址

https://github.com/jiaxwu/gommon/tree/main/container/ringbuffer

原文鏈接:https://juejin.cn/post/7138675261649715236

相關推薦

欄目分類
最近更新