环形队列

基础知识

环形队列是什么

  1. 数据是按照 “先进先出” 的原则进行操作
  2. 是环形的,即队列头部的上个元素是队列尾部
  3. 通常是容纳元素数固定的一个闭环

原理

内存上没有环形的结构,因此环形队列实上是数组的线性空间来实现。那当数据到了尾部如何处理呢?它将转回到0位置来处理。这个的转回是通过取模操作来执行的。因此环列队列的是逻辑上将数组元素 q[0]q[MAXN-1] 连接,形成一个存放队列的环形空间。

为了方便读写,还要用数组下标来指明队列的读写位置。

场景

  • 环形队列主要应用在单线程读或者写
  • 一般应用于需要高效且频繁进行多线程通信传递数据的场景,例如:linux捕包、发包等等
  • 广泛用于网络数据收发,和不同程序间数据交换(比如内核与应用程序大量交换数据,从硬件接收大量数据)均使用了环形队列

优点

保证元素是先进先出的

是由队列的性质保证的,在环形队列中通过对队列的顺序访问保证。

元素空间可以重复利用

因为一般的环形队列都是一个元素数固定的一个闭环,可以在环形队列初始化的时候分配好确定的内存空间,当进队或出队时只需要返回指定元素内存空间的地址即可,这些内存空间可以重复利用,避免频繁内存分配和释放的开销。

为多线程数据通信提供了一种高效的机制

在最典型的生产者消费者模型中,如果引入环形队列,那么生成者只需要生成“东西”然后放到环形队列中即可,而消费者只需要从环形队列里取“东西”并且消费即可,没有任何锁或者等待,巧妙的高效实现了多线程数据通信。

工作场景

实际环形队列在工作时有3种情况:

入队速度=出队速度

这是环形队列的常态,即入队速度和出队速度大致一样,即使某个突然时刻入队速度陡然变高或者出队速度陡然变低,都能通过队列这个缓冲区把这些数据先存起来,等到能处理的时候再处理。

入队速度>出队速度

在这种情况下,队列 写入速度 > 读取速度,想象当这种状态持续一段时间之后,队列中大多数全是写入但没读取的元素,当又一个新的元素产生时,可以把这个新元素 drop 掉或者放在另一个缓冲区保存起来,这种情况的出现不是个好事情,说明你需要对出队处理元素的算法或逻辑优化处理速度了。

入队速度<出队速度

在这种情况下,队列 读取速度 > 写入速度,这种情况说明程序出队处理元素的速度很快,这是比较好的情况,唯一不足的是读取队列的时候可能经常会轮询队列是否有新的元素,造成 cpu 占用过高。

无锁环形队列的实现

环形队列的存储结构

链表和线性表都是可以的,但几乎都用线性表实现,比链表快很多,原因也是显而易见的,因为访问链表需要挨个遍历。

读写index

有2个index很重要,一个是写入index,标示了当前可以写入元素的index,入队时使用。一个是读取index,标示了当前可以读取元素的index,出队时使用。

元素状态切换

有种很巧妙的方法,就是在队列中每个元素的头部加一个元素标示字段,标示这个元素是可读还是可写,而这个的关键就在于何时设置元素的可读可写状态,参照linux内核实现原理,当这个元素读取完之后,要设置可写状态,当这个元素写入完成之后,要设置可读状态。

实现

下面是 Ruby 的简单实现, 出队和入队的时候都没有考虑队列中的元素是否已满或是否存在。

class RingBuffer < Array
  attr_reader :max_size

  def initialize(max_size, enum = nil)
    @max_size = max_size
    enum.each { |e| self << e } if enum
  end

  def <<(el)
    if self.size < @max_size || @max_size.nil?
      super
    else
      self.shift
      self.push(el)
    end
  end

  alias :push :<<
end

相关阅读

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Jul 11, 2018

奉献爱心