shinway / internal /ringbuffer /ringbuffer.go
sshinmen's picture
Configure for HF Spaces: port 7860, cloud deploy mode, PORT env support
5e6ff88
Raw
History Blame Contribute Delete
5.41 kB
// Package ringbuffer provides lock-free ring buffer implementations for high-performance streaming.
// These are used for stream chunk queuing with minimal synchronization overhead.
package ringbuffer
import (
"runtime"
"sync/atomic"
)
const (
// CacheLineSize is the typical CPU cache line size (64 bytes on most architectures)
CacheLineSize = 64
)
// RingBuffer is a single-producer single-consumer (SPSC) lock-free ring buffer.
// It uses atomic operations for synchronization, making it suitable for high-throughput
// scenarios where minimal latency is critical.
type RingBuffer[T any] struct {
// Padding to prevent false sharing between head and tail
_ [CacheLineSize]byte
// head is the read position (consumer)
head atomic.Uint64
_ [CacheLineSize - 8]byte // Padding to separate head and tail to different cache lines
// tail is the write position (producer)
tail atomic.Uint64
_ [CacheLineSize - 8]byte // Padding
buffer []T
mask uint64
}
// New creates a new RingBuffer with capacity rounded up to power of 2.
func New[T any](capacity int) *RingBuffer[T] {
if capacity <= 0 {
capacity = 1024
}
// Round up to power of 2
capacity = roundUpPowerOf2(capacity)
return &RingBuffer[T]{
buffer: make([]T, capacity),
mask: uint64(capacity - 1),
}
}
// Push adds an item to the ring buffer.
// Returns false if the buffer is full.
// This method is safe for single-producer usage.
func (r *RingBuffer[T]) Push(item T) bool {
tail := r.tail.Load()
head := r.head.Load()
// Check if full
if tail-head >= uint64(len(r.buffer)) {
return false
}
// Write item
r.buffer[tail&r.mask] = item
// Update tail with memory barrier
r.tail.Store(tail + 1)
return true
}
// Pop removes and returns an item from the ring buffer.
// Returns (zero value, false) if the buffer is empty.
// This method is safe for single-consumer usage.
func (r *RingBuffer[T]) Pop() (T, bool) {
var zero T
head := r.head.Load()
tail := r.tail.Load()
// Check if empty
if head == tail {
return zero, false
}
// Read item
item := r.buffer[head&r.mask]
// Update head with memory barrier
r.head.Store(head + 1)
return item, true
}
// TryPush attempts to add an item with a spin-wait (for batch operations).
func (r *RingBuffer[T]) TryPush(item T, spins int) bool {
for i := 0; i < spins; i++ {
if r.Push(item) {
return true
}
runtime.Gosched()
}
return false
}
// TryPop attempts to remove an item with a spin-wait.
func (r *RingBuffer[T]) TryPop(spins int) (T, bool) {
var zero T
for i := 0; i < spins; i++ {
if item, ok := r.Pop(); ok {
return item, true
}
runtime.Gosched()
}
return zero, false
}
// Len returns the current number of items in the buffer.
func (r *RingBuffer[T]) Len() int {
return int(r.tail.Load() - r.head.Load())
}
// Cap returns the capacity of the buffer.
func (r *RingBuffer[T]) Cap() int {
return len(r.buffer)
}
// IsEmpty returns true if the buffer is empty.
func (r *RingBuffer[T]) IsEmpty() bool {
return r.head.Load() == r.tail.Load()
}
// IsFull returns true if the buffer is full.
func (r *RingBuffer[T]) IsFull() bool {
return r.tail.Load()-r.head.Load() >= uint64(len(r.buffer))
}
// Reset clears the buffer (not thread-safe).
func (r *RingBuffer[T]) Reset() {
r.head.Store(0)
r.tail.Store(0)
for i := range r.buffer {
var zero T
r.buffer[i] = zero
}
}
// MPMCQueue is a multi-producer multi-consumer lock-free queue.
// It uses a different algorithm that supports concurrent access from multiple goroutines.
type MPMCQueue[T any] struct {
_ [CacheLineSize]byte
slots []slot[T]
mask uint64
sendx atomic.Uint64
recvx atomic.Uint64
_ [CacheLineSize]byte
}
type slot[T any] struct {
turn atomic.Uint64
item T
_ [CacheLineSize - 16]byte
}
// NewMPMC creates a new multi-producer multi-consumer queue.
func NewMPMC[T any](capacity int) *MPMCQueue[T] {
if capacity <= 0 {
capacity = 1024
}
capacity = roundUpPowerOf2(capacity)
q := &MPMCQueue[T]{
slots: make([]slot[T], capacity),
mask: uint64(capacity - 1),
}
// Initialize turn counters
for i := range q.slots {
q.slots[i].turn.Store(uint64(i))
}
return q
}
// Enqueue adds an item to the queue.
func (q *MPMCQueue[T]) Enqueue(item T) bool {
for {
sendx := q.sendx.Load()
slot := &q.slots[sendx&q.mask]
turn := slot.turn.Load()
if turn == sendx {
if q.sendx.CompareAndSwap(sendx, sendx+1) {
slot.item = item
slot.turn.Store(sendx + 1)
return true
}
} else if turn < sendx {
// Queue is full
return false
}
// Slot not ready, retry
runtime.Gosched()
}
}
// Dequeue removes and returns an item from the queue.
func (q *MPMCQueue[T]) Dequeue() (T, bool) {
var zero T
for {
recvx := q.recvx.Load()
slot := &q.slots[recvx&q.mask]
turn := slot.turn.Load()
if turn == recvx+1 {
if q.recvx.CompareAndSwap(recvx, recvx+1) {
item := slot.item
var empty T
slot.item = empty
slot.turn.Store(recvx + q.mask + 1)
return item, true
}
} else if turn < recvx+1 {
// Queue is empty
return zero, false
}
// Slot not ready, retry
runtime.Gosched()
}
}
// Len returns approximate queue length.
func (q *MPMCQueue[T]) Len() int {
return int(q.sendx.Load() - q.recvx.Load())
}
func roundUpPowerOf2(n int) int {
if n <= 0 {
return 1
}
if n&(n-1) == 0 {
return n
}
p := 1
for p < n {
p <<= 1
}
return p
}