无锁循环数组

6

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

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

那么我会做类似这样的事情:

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

请注意,如果数组已满,则旧值将被覆盖,我对此没有问题。
有人看到这个设计存在问题吗?我怀疑可能存在我没有发现的竞争条件。
4个回答

5
一种更简单的方法是使用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;
 }

2
如果增量发生在最后一次对数组的写入之前,我们如何确保最后一次写入对于其他操作是可见的? - spongebob
@FrancescoMenzani 这并不能保证,但由于读者和写者之间没有协调,如果您需要这样的功能,您可能需要更复杂的东西。 - Peter Lawrey

4

循环缓冲区等很多东西。 - user3458
Disruptor 不一定是无锁的。如果你不想让 CPU 一直运行在 100% 的状态下,你需要使用 EMPTY 和 FULL 互斥策略。 - Johnny V

2

是的,存在竞态条件。

假设 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

当第二个线程到达array[idx] = value时(以及在i溢出之前的所有后续调用add()),会导致ArrayIndexOutOfBoundsException

@Peter Lawrey提出的解决方案不会遇到这个问题。


1
如果您遵循以下限制:
  • 在任何时候只允许一个线程修改头指针
  • 在任何时候只允许一个线程修改尾指针
  • 从空队列出队会返回一个指示未执行任何操作的值
  • 当队列已满时,入队会返回一个指示未执行任何操作的值
  • 不保留存储在队列中的数据数量计数
  • “浪费”数组中永远不会使用的一个索引,以便无需计数即可知道数组是满还是空。

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

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

该条件是测试是否为空或已满。

考虑空队列表示head == tail; 考虑满队列表示tail == head - 1模数组大小。Enqueue必须检查队列是否已满,dequeue必须检查队列是否为空。您需要在数组中浪费一个索引来检测full和empty之间的差异-如果您将其入队到最后一个桶中,则full将是head == tail,而empty将是head == tail,现在您死锁了-您认为自己同时为空和满,因此不会完成任何工作。
在执行这些检查时,有可能更新一个值而进行比较。但是由于这两个值单调递增,因此没有正确性问题:
- 如果在dequeue方法中,head == tail计算为true在比较期间,但是tail随后向前移动,没有问题-您认为数组为空,但实际上并不是,但没关系,您只需从dequeue方法返回false并重试即可。 - 如果在enqueue方法中,tail == head - 1计算为true,但是随后头部增加,那么您将认为数组已满,而实际上并不是,但同样,没关系,您只需从enqueue返回false并重试即可。
这是我在多年前在Dr. Dobb's上发现并使用的设计,它一直为我服务良好:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接