繁体   English   中英

带翻转缓冲区的无锁容器

[英]Lock-free container with flipping buffers

对于我的一个必须支持并发读取和写入的项目,我需要一个能够缓冲项目的容器,直到消费者一次获取每个当前缓冲的项目。 由于生产者应该能够产生数据而不管消费者是否读取当前缓冲区我想出了一个自定义实现,在AtomicReference的帮助下,将每个条目添加到支持ConcurrentLinkedQueue直到执行翻转,导致当前条目在存储具有空队列和元数据的新条目时返回,以原子方式存储在该AtomicReference

我想出了一个解决方案,例如

public class FlippingDataContainer<E> {

  private final AtomicReference<FlippingDataContainerEntry<E>> dataObj = new AtomicReference<>();

  public FlippingDataContainer() {
    dataObj.set(new FlippingDataContainerEntry<>(new ConcurrentLinkedQueue<>(), 0, 0, 0));
  }

  public FlippingDataContainerEntry<E> put(E value) {
    if (null != value) {
      while (true) {
        FlippingDataContainerEntry<E> data = dataObj.get();
        FlippingDataContainerEntry<E> updated = FlippingDataContainerEntry.from(data, value);
        if (dataObj.compareAndSet(data, updated)) {
          return merged;
        }
      }
    }
    return null;
  }

  public FlippingDataContainerEntry<E> flip() {
    FlippingDataContainerEntry<E> oldData;
    FlippingDataContainerEntry<E> newData = new FlippingDataContainerEntry<>(new ConcurrentLinkedQueue<>(), 0, 0, 0);
    while (true) {
      oldData = dataObj.get();
      if (dataObj.compareAndSet(oldData, newData)) {
        return oldData;
      }
    }
  }

  public boolean isEmptry() {
    return dataObj.get().getQueue().isEmpty();
  }
}

由于需要将当前值推送到后备队列,因此现在需要格外小心。 from(data, value)方法的当前实现看起来像这样:

static <E> FlippingDataContainerEntry<E> from(FlippingDataContainerEntry<E> data, E value) {
  Queue<E> queue = new ConcurrentLinkedQueue<>(data.getQueue());
  queue.add(value);
  return new FlippingDataContainerEntry<>(queue,
      data.getKeyLength() + (value.getKeyAsBytes() != null ? value.getKeyAsBytes().length : 0),
      data.getValueLength() + (value.getValueAsBytes() != null ? value.getValueAsBytes().length : 0),
      data.getAuxiliaryLength() + (value.getAuxiliaryAsBytes() != null ? value.getAuxiliaryAsBytes().length : 0));
}

由于可能由其他线程在该线程执行更新之前更新值引起的重试,我需要在每次写入尝试时复制实际队列,否则即使原子引用不能,也会将条目添加到共享队列中” t 得到更新。 因此,简单地将值添加到共享队列可能导致值条目被多次添加到队列中,而实际上它只应该出现一次。

复制整个队列是一项非常昂贵的任务,所以我只修改了当前队列,而不是在from(data, value)方法中复制队列,而不是将 value 元素添加到执行时块中的共享队列发生更新:

public FlippingDataContainerEntry<E> put(E value) {
  if (null != value) {
    while (true) {
      FlippingDataContainerEntry<E> data = dataObj.get();
      FlippingDataContainerEntry<E> updated = FlippingDataContainerEntry.from(data, value);
      if (data.compareAndSet(data, updated)) {
        updated.getQueue().add(value);
        return updated;
      }
    }
  }
  return null;
}

from(data, value)我现在只设置队列而不直接添加值元素

static <E> FlippingDataContainerEntry<E> from(FlippingDataContainerEntry<E> data, E value) {
  return new FlippingDataContainerEntry<>(data.getQueue(),
      data.getKeyLength() + (value.getKeyAsBytes() != null ? value.getKeyAsBytes().length : 0),
      data.getValueLength() + (value.getValueAsBytes() != null ? value.getValueAsBytes().length : 0),
      data.getAuxiliaryLength() + (value.getAuxiliaryAsBytes() != null ? value.getAuxiliaryAsBytes().length : 0));
}

虽然与复制队列的代码相比,这允许测试运行速度快 10 倍以上,但它也经常无法通过消耗测试,因为现在在消费者线程翻转队列并处理之后可能会立即将值元素添加到队列中数据,因此并非所有项目似乎都被消耗了。

现在的实际问题是,是否可以避免复制后备队列以获得性能提升,同时仍然允许使用无锁算法自动更新队列的内容,从而避免在中途丢失一些条目?

首先,让我们陈述显而易见的 - 最好的解决方案是避免编写任何此类自定义类。 也许像java.util.concurrent.LinkedTransferQueue这样简单的东西也能正常工作,而且不容易出错。 如果LinkedTransferQueue不起作用,那么LMAX 干扰器或类似的东西呢? 您是否查看过现有的解决方案?

如果您仍然需要/想要自定义解决方案,那么我有一个略有不同的方法的草图,可以避免复制:

这个想法是让put操作围绕一些原子变量旋转,试图设置它。 如果一个线程设法设置它,那么它获得对当前队列的独占访问权,这意味着它可以附加到它。 追加后,它重置原子变量以允许其他线程追加。 它基本上是一个自旋锁 这样,线程之间的争用发生在追加到队列之前,而不是之后。

我需要在每次写入尝试时复制实际队列

你的想法听起来像 RCU ( https://en.wikipedia.org/wiki/Read-copy-update )。 通过为您解决释放问题(我认为),Java 被垃圾收集使 RCU 更容易。

如果我从快速浏览您的问题中正确理解,那么您的“读者”实际上想为自己“声明”容器的全部当前内容。 这也使他们成为有效的编写者,但他们可以构建一个空容器,而不是读取 + 复制,并以原子方式交换顶级引用以指向该容器。 (因此要求旧容器进行独占访问。)

RCU 的一大好处是容器数据结构本身不必到处都是原子的; 一旦你引用了它,其他人就不会修改它。


唯一棘手的部分是当作者想要向非空容器添加新内容时。 然后您复制现有容器并修改副本,并尝试将 CAS(比较交换,即compareAndSet() )更新后的副本放入共享的顶级AtomicReference

作家不能只是无条件地交换,因为它可能最终得到一个非空的容器并且无处可放。 除非作者可以继续处理一批工作并旋转等待读者清空队列......


我在这里假设您的作家有成批的工作要立即排队; 否则 RCU 对作家来说可能太贵了。 抱歉,如果我错过了您的问题中排除了这一点的细节。 我不经常使用 Java,所以我只是快速写一下,以防万一。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM