【问题标题】:Lock Free Circular Array无锁圆阵列
【发布时间】:2014-01-02 19:42:25
【问题描述】:

我正在考虑实现一个无锁循环数组。一个问题是以无锁方式维护头和尾指针。我想到的代码是:

int circularIncrementAndGet(AtomicInteger i) {
    i.compareAndSet(array.length - 1, -1);
    return i.incrementAndGet();
}

然后我会做这样的事情:

void add(double value) {
    int idx = circularIncrementAndGet(tail);
    array[idx] = value;
}

(请注意,如果数组已满,旧值将被覆盖,我可以接受)。

有人认为这种设计有问题吗?我怀疑可能存在我没有看到的比赛条件。

【问题讨论】:

    标签: java lock-free circular-buffer


    【解决方案1】:

    更简单的方法是使用 2 的幂并执行以下操作。

     final double[] array;
     final int sizeMask;
     final AtomicInteger i = new AtomicInteger();
    
     public CircularBuffer(int size) {
          assert size > 1 && ((size & (size -1)) == 0); // test power of 2.
          array = new double[size];
          sizeMask = size -1;
     }
    
     void add(double value) {
         array[i.getAndIncrement() & sizeMask] = value;
     }
    

    【讨论】:

    • 如果增量发生在之前,我们如何确保对数组的最后一次写入是可见的?
    • @FrancescoMenzani 它不能保证它,但由于读者和作者之间没有协调,如果你需要的话,你可能想要更复杂的东西。
    【解决方案2】:

    查看中断器:http://lmax-exchange.github.io/disruptor/,它是 Java 中的开源无锁循环缓冲区。

    【讨论】:

    • 循环缓冲区等等。
    • Disruptor 也不一定是无锁的。如果您不想以 100% 的速度旋转 CPU,则需要 EMPTY 和 FULL 互斥策略。
    【解决方案3】:

    是的,有一个竞争条件。

    i = array.length - 2,两个线程进入circularIncrementAndGet()

    Thread 1: i.compareAndSet(array.length - 1, -1) results in i = array.length - 2
    Thread 2: i.compareAndSet(array.length - 1, -1) results in i = array.length - 2
    Thread 1: i.incrementAndGet() results in i = array.length - 1
    Thread 2: i.incrementAndGet() results in i = array.length
    

    当线程 2 到达 array[idx] = value 时导致 ArrayIndexOutOfBoundsException(以及在所有后续调用 add() 直到 i 溢出)。

    @Peter Lawrey 提出的解决方案不存在这个问题。

    【讨论】:

      【解决方案4】:

      如果你坚持以下限制:

      • 任何时候只允许一个线程修改头指针
      • 任何时候只允许一个线程修改尾指针
      • Dequeue-on-empty 给出一个返回值,表明什么都没做
      • Enqueue-on-full 给出一个返回值,表明什么都没做
      • 您不会计算队列中存储了多少值。
      • 您在数组中“浪费”了一个永远不会使用的索引,这样您就可以知道数组何时已满或何时为空,而无需计数。

      可以实现循环数组/队列。

      入队线程拥有尾指针。出队线程拥有头指针。除了一个条件,这两个线程目前没有共享任何状态,所以没有问题。

      该条件正在测试空虚或饱满。

      认为空意味着 head == tail;考虑 full 意味着 tail == head - 1 模数组大小。 Enqueue 必须检查队列是否已满,dequeue 必须检查队列是否为空。您需要在数组中浪费一个索引来检测完整和空之间的差异 - 如果您排队进入最后一个存储桶,那么完整将是 head == tail 并且空将是 head == tail 现在你死锁了 - 你认为你是既空又满,所以没有工作可以完成。

      在执行这些检查时,可能会在比较时更新一个值。但是由于这两个值是单调递增的,所以不存在正确性问题:

      • 如果在 dequeue 方法中,在比较期间 head == tail 计算为真,但 tail 紧接着向前移动,没问题 - 你认为数组是空的,但实际上不是,但没什么大不了的,您只需从 dequeue 方法返回 false 并重试。
      • 如果在 enqueue 方法中,tail == head - 1 计算为 true,但在此之后 head 增加,那么您会认为数组已满,但实际上并非如此,但同样,不大不了,你只会从 enqueue 中返回 false 并重试。

      这是我多年前在 Dobb 博士那里找到的实现背后使用的设计,它对我很有帮助:

      http://www.drdobbs.com/parallel/lock-free-queues/208801974

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-07-25
        • 2017-03-30
        • 1970-01-01
        • 2017-04-21
        • 2011-08-30
        • 1970-01-01
        相关资源
        最近更新 更多