mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2391 字
6 分钟
环形缓冲区(Ring Buffer)
2026-06-13

一、为什么需要环形缓冲区#

假设你在写一个音频播放器,生产者线程从网络不断接收音频数据,消费者线程按采样率读取播放。最直觉的做法是用一个动态数组当队列:新数据来了就 push,播放完就 shift

问题在于 shift。JavaScript 的 Array.shift() 或 Python 的 list.pop(0) 会把剩余所有元素向前搬移一位,时间复杂度 O(n)。音频采样率 44.1kHz,每秒要出队 44100 次,每次搬移几千个元素——CPU 全在搬数据,播放直接卡顿。

换成链表呢?出队倒是 O(1) 了,但每个节点都要 malloc/free,堆分配的开销在实时系统里不可接受。更麻烦的是链表节点散落在内存各处,CPU 缓存命中率极低,指针追踪的开销比搬数据还大。

环形缓冲区解决了这两个问题:固定大小的数组,读写指针循环推进,入队出队都是 O(1),零内存分配。它不是什么新发明,而是实时系统和高性能队列的基石——从 Linux 内核的 ftrace 到金融交易系统的无锁队列,底层都是它。

二、现实类比#

想象回转寿司店的传送带。传送带是固定长度的环形轨道,寿司师傅在某个位置放寿司(写),顾客在另一个位置取寿司(读)。师傅放一盘,写指针往前走一格;顾客取一盘,读指针往前走一格。轨道是环形的,指针走到尽头自动回到起点,不需要把剩下的寿司往前挪。

关键约束:传送带只有这么多位置。如果师傅放得太快、顾客吃得太慢,传送带就满了——要么师傅等一等(阻塞),要么把最旧的寿司扔掉(覆盖)。如果顾客吃得太快,传送带就空了——顾客只能等(阻塞)。这就是环形缓冲区的核心语义:固定容量、先进先出、读写解耦

三、核心思想#

环形缓冲区用一块固定大小的数组存储数据,维护两个指针:

  • head(读指针):下一个可读位置,出队后 head = (head + 1) % capacity
  • tail(写指针):下一个可写位置,入队后 tail = (tail + 1) % capacity

指针走到数组末尾时,取模运算让它回到开头,形成环形。入队写 tail 位置再推进 tail,出队读 head 位置再推进 head,两者互不干扰——这正是无锁单生产者单消费者(SPSC)队列的基础。

flowchart LR subgraph Ring["环形缓冲区(capacity = 8)"] direction LR S0["0"] --- S1["1"] --- S2["2"] --- S3["3"] S3 --- S4["4"] --- S5["5"] --- S6["6"] --- S7["7"] S7 --- S0 end W["写指针 tail"] --> S4["4"] R["读指针 head"] --> S1["1"] S1_"1 ✓" --> S2_"2 ✓"] --> S3_"3 ✓" style S1_ fill:#90EE90 style S2_ fill:#90EE90 style S3_ fill:#90EE90 style S4 fill:#FFB6C1

上图中,位置 1-3 是已读数据(绿色),位置 4 是下一个写入位置(粉色),位置 5-7 和 0 是空闲位置。tail 追着 head 跑,head 追着 tail 跑,谁也追不上谁的时候就是满或空。

空 vs 满#

这里有个经典问题:当 head == tail 时,到底是空还是满?三种解法:

  • 维护计数器:额外记录元素个数,count == 0 为空,count == capacity 为满。最直觉,但多一个变量要同步,无锁场景下增加复杂度。
  • 浪费一个槽位tail 的下一个位置是 head 时视为满,实际可用容量为 capacity - 1。简单直接,是嵌入式系统最常用的方案。
  • 单调递增序列号(Disruptor 方案):headtail 不取模,一直递增,用 sequence & (capacity - 1) 算实际下标。空满判断靠序列号差值,不需要额外变量,也不浪费槽位。

2 的幂优化#

如果容量是 2 的幂,取模可以用位与替代:index % capacity 等价于 index & (capacity - 1)。位与运算是单条 CPU 指令,取模可能触发除法指令(数十个时钟周期)。LMAX Disruptor 正是利用这个技巧,将序列号映射到数组下标:

// Disruptor RingBuffer.java 中的核心映射
E elementAt(long sequence) {
return entries[BUFFER_PAD + (int)(sequence & indexMask)];
}

其中 indexMask = capacity - 1capacity 保证是 2 的幂。

复杂度#

操作时间复杂度空间复杂度
入队 enqueueO(1)O(capacity)
出队 dequeueO(1)O(capacity)
查看队首 peekO(1)-
判空/判满O(1)-

四、变体与对比#

环形缓冲区不是唯一的队列实现。把它和动态数组队列、链表队列放在一起看,差异就很明显:

特性环形缓冲区动态数组队列链表队列
入队O(1)O(1) 均摊O(1)
出队O(1)O(n) 或 O(1) 均摊O(1)
内存分配零(初始化后)扩容时批量分配每次入队分配
缓存友好性极好(连续内存)差(指针追踪)
容量固定动态增长动态增长
无锁友好是(SPSC)困难
内存开销仅数组数组 + 扩容冗余节点 + 指针

动态数组队列的出队性能取决于实现:shift() 是 O(n),双指针实现是 O(1) 但扩容时需要搬移。链表队列虽然入队出队都是 O(1),但每个节点的堆分配在实时场景下是硬伤——malloc 的耗时不可预测,GC 介入时更不可控。

环形缓冲区的代价是固定容量。如果你不知道数据量的上界,或者需要随机访问中间元素,它就不是最佳选择。

五、多语言实现#

Go 实现#

Go 版本采用「浪费一个槽位」策略判空满,容量强制为 2 的幂以启用位与优化:

package ringbuffer
import "errors"
// RingBuffer 是固定大小的环形队列,容量为 2 的幂
type RingBuffer[T any] struct {
buf []T
head uint64 // 读指针
tail uint64 // 写指针
mask uint64 // capacity - 1,用于位与取模
}
// New 创建容量为 2^k 的环形缓冲区
// 传入的 size 会向上取整到最近的 2 的幂
func New[T any](size uint64) *RingBuffer[T] {
// 向上取整到 2 的幂
cap := uint64(1)
for cap < size {
cap <<= 1
}
return &RingBuffer[T]{
buf: make([]T, cap),
mask: cap - 1,
}
}
// Enqueue 入队,满时返回错误
func (rb *RingBuffer[T]) Enqueue(val T) error {
next := rb.tail + 1
// 浪费一个槽位:tail+1 == head 表示满
if next&rb.mask == rb.head&rb.mask {
return errors.New("ring buffer: 已满")
}
rb.buf[rb.tail&rb.mask] = val
rb.tail = next
return nil
}
// Dequeue 出队,空时返回错误
func (rb *RingBuffer[T]) Dequeue() (T, error) {
var zero T
if rb.tail == rb.head {
return zero, errors.New("ring buffer: 已空")
}
val := rb.buf[rb.head&rb.mask]
rb.buf[rb.head&rb.mask] = zero // 避免引用残留,帮助 GC
rb.head++
return val, nil
}
// Len 返回当前元素个数
func (rb *RingBuffer[T]) Len() uint64 {
// tail 和 head 不取模,差值就是元素数
return rb.tail - rb.head
}

几个设计要点:

  • headtailuint64 一直递增,不回绕。实际下标通过 & rb.mask 计算,这就是 Disruptor 的单调序列号思路。
  • Enqueuenext&rb.mask == rb.head&rb.mask 判断满——等价于「写指针的下一个位置等于读指针」,浪费了一个槽位。
  • Dequeue 中把出队位置置为零值,防止引用类型数据无法被 GC 回收。这是 Go 环形缓冲区的常见陷阱。

TypeScript 实现#

TypeScript 版本同样使用 2 的幂容量和位与取模,但用 count 计数器判空满,更易理解:

export class RingBuffer<T> {
private buf: (T | undefined)[];
private head = 0; // 读指针
private tail = 0; // 写指针
private count = 0; // 当前元素数
private mask: number; // capacity - 1
constructor(capacity: number) {
// 向上取整到 2 的幂
let cap = 1;
while (cap < capacity) cap <<= 1;
this.buf = new Array<T | undefined>(cap);
this.mask = cap - 1;
}
/** 入队,满时返回 false */
enqueue(val: T): boolean {
if (this.count === this.buf.length) return false;
this.buf[this.tail & this.mask] = val;
this.tail++;
this.count++;
return true;
}
/** 出队,空时返回 undefined */
dequeue(): T | undefined {
if (this.count === 0) return undefined;
const val = this.buf[this.head & this.mask];
this.buf[this.head & this.mask] = undefined; // 释放引用
this.head++;
this.count--;
return val;
}
/** 查看队首元素 */
peek(): T | undefined {
if (this.count === 0) return undefined;
return this.buf[this.head & this.mask];
}
get length(): number {
return this.count;
}
}

TypeScript 版本用 count 判空满,逻辑更清晰,适合应用层使用。代价是多维护一个变量,在无锁场景下需要额外同步。如果只用于单线程,count 方案是最省心的选择。

六、生产验证#

LMAX Disruptor#

LMAX Disruptor 是金融交易系统 LMAX 开源的高性能进程内消息传递库,核心就是环形缓冲区。它的 RingBuffer.java 用单调递增的序列号代替传统读写指针,通过 sequence & indexMask 映射到数组下标,配合缓存行填充(cache line padding)避免伪共享。Disruptor 官方宣称单线程每秒可处理 600 万笔订单,比传统加锁队列快 3 个数量级。

Linux 内核 ftrace#

Linux 内核的性能追踪工具 ftrace 底层使用环形缓冲区记录追踪事件。每个 CPU 核心有独立的 ring_buffer_per_cpu 结构,包含 head_pagetail_pagecommit_pagereader_page 四个指针,实现无锁的 per-CPU 写入。这种设计让 ftrace 在高负载下依然能低开销地记录内核事件,不会因为锁竞争影响被追踪的系统本身。

  • ring_buffer.cring_buffer_per_cputrace_buffer 核心结构定义

Workiva Go Data Structures#

Workiva 的 go-datastructures 库提供了生产级的 Go 环形缓冲区实现,支持阻塞和非阻塞两种入队模式,内部使用 mutex + condition variable 实现线程安全。该库被 Workiva 的金融数据平台广泛使用,处理实时数据流。

  • ring.goRingBuffer 结构体定义
  • Put/Get — 入队出队实现

七、小结#

适合使用环形缓冲区的场景:

  • 固定大小的生产者-消费者队列,尤其是音频/视频/网络流处理
  • 单生产者单消费者(SPSC)无锁队列,读写指针天然解耦
  • 嵌入式和实时系统,要求零内存分配、确定性延迟
  • 遥测和日志采集,允许覆盖最旧数据(overwrite-oldest),永不阻塞

不适合使用环形缓冲区的场景:

  • 数据量无法预估上界,需要动态增长——环形缓冲区容量固定,满了只能阻塞或丢弃
  • 需要按键随机访问,而非先进先出——环形缓冲区只支持顺序读写
  • 元素大小差异大——环形缓冲区每个槽位等长,变长元素需要额外处理(外部分配 + 存索引)
  • 多生产者多消费者(MPMC)场景——简单环形缓冲区的无锁特性仅限 SPSC,MPMC 需要更复杂的协调机制

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

环形缓冲区(Ring Buffer)
https://blog.souloss.com/posts/programming/data-structures/data-structures-ring-buffer/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时