Ring Buffer
环境队列
环形队列,Circular Queue,或者也被称为 Ring Buffer,Circular Buffer,Cyclic Buffer 等等,是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。它的学术上的研讨不在本文的展开范围内,简而言之,不但是通信开发(Socket,TCP/IP,RPC开发)中,在内核的IPC中,在视频音频播放中,在类似于 Map-Reduce 的工作队列排列中,在广泛的各种需要数据流数据结构表示的场景中,环形队列往往是一种最佳选择。
比如网卡的处理缓存区
特性
计算机内存是线性而非是环状的,因而当环形队列被实现到内存中时,通常会用一个连续的内存块来表示,同时通过使用一对额外的指针来指示首、尾位置,从而在逻辑上将其卷曲起来
首先,环形队列是有界的。环形队列适合于事先明确了缓冲区的最大容量的情形。扩展一个环形队列的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。
其次,环形队列是一种 FIFO 数据结构。它和普通 FIFO 队列数据结构的不同就在于队尾连接着队首,当入列元素位于队尾时,该元素将被回绕并放在队首的位置,从而完成有界性限定。而普通的 FIFO 队列总是增长队尾以入列新元素,并不限制队列的有效长度。
高性能
因为是FIFO的结构,所以有一个特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。
相反,一个非环形队列(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移(这也并非必须如此,链表可以避免数据搬移带来的读写性能差,但也会进一步衍生出新的问题——其链表指针操作难以无锁化导致SMP场景中读写性能差)。
正是因为这一特性,环形队列具有先天优势,无需数据搬移(也避免了动态内存分配)。而由于环形队列通常仅仅需要固定的首尾两个指针(一般的实现中,这两个指针实际上都是数组的下标值)即可访问,因而很容易解决 SMP 场景中的无锁化问题
实现方式
一般的,圆形缓冲区需要4个指针:
- 在内存中实际开始位置;
- 在内存中实际结束位置,也可以用缓冲区长度代替;
- 存储在缓冲区中的有效数据的开始位置(读指针);
- 存储在缓冲区中的有效数据的结尾位置(写指针)。
读指针、写指针可以用整型值来表示。
Disruptor的设计方案
Disruptor通过以下设计来解决队列速度慢的问题:
环形数组结构
为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。
元素位置定位
数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
无锁设计
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。
一般情况下队列可以使用JDK提供的阻塞队列,在高性能场景下 disruptor 使用精巧的设计,通过无锁(CAS)提升了并发。