无锁有界MPMC环形缓冲区故障

放任

我一直在无锁的多生产者多消费者环形缓冲区上狂奔(我的尝试)。这个想法的基础是使用无符号字符型和无符号短类型的先天溢出,将元素缓冲区固定为这些类型中的任何一个,然后您可以自由循环回到环形缓冲区的开头。

问题是-我的解决方案不适用于多个生产者(尽管适用于N个消费者,也适用于单个生产者单个消费者)。

#include <atomic>

template<typename Element, typename Index = unsigned char> struct RingBuffer
{
  std::atomic<Index> readIndex;
  std::atomic<Index> writeIndex;
  std::atomic<Index> scratchIndex;
  Element elements[1 << (sizeof(Index) * 8)];

  RingBuffer() :
    readIndex(0),
    writeIndex(0),
    scratchIndex(0)
  {
    ;
  }

  bool push(const Element & element)
  {
    while(true)
    {
      const Index currentReadIndex = readIndex.load();
      Index currentWriteIndex = writeIndex.load();
      const Index nextWriteIndex = currentWriteIndex + 1;
      if(nextWriteIndex == currentReadIndex)
      {
        return false;
      }

      if(scratchIndex.compare_exchange_strong(
        currentWriteIndex, nextWriteIndex))
      {
        elements[currentWriteIndex] = element;
        writeIndex = nextWriteIndex;
        return true;
      }
    }
  }

  bool pop(Element & element)
  {
    Index currentReadIndex = readIndex.load();

    while(true)
    {
      const Index currentWriteIndex = writeIndex.load();
      const Index nextReadIndex = currentReadIndex + 1;
      if(currentReadIndex == currentWriteIndex)
      {
        return false;
      }

      element = elements[currentReadIndex];

      if(readIndex.compare_exchange_strong(
        currentReadIndex, nextReadIndex))
      {
        return true;
      }
    }
  }
};

编写的主要思想是使用临时索引'scratchIndex',该索引充当伪锁,以便在更新writeIndex并允许任何其他生产者进行更新之前,仅一次允许一个生产者在任何时候将其复制构造到元素缓冲区中。 。在我被称为异教徒以暗示我的方法是“无锁的”之前,我意识到这种方法并不是完全无锁的,但实际上(如果可行的话)它比使用普通互斥锁要快得多!

我知道这里有一个(更复杂的)MPMC环形缓冲区解决方案,http: //www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue ,但是我确实在尝试用我的想法进行比较反对这种方法,找出每个方法的优势(或者实际上我的方法是否完全失败!)。

我尝试过的事情;

  • 使用compare_exchange_weak
  • 使用更精确的std :: memory_order匹配我想要的行为
  • 在我拥有的各种索引之间添加缓存行填充
  • 使元素std :: atomic而不只是Element数组

我敢肯定,这归结为我的脑海中关于如何使用原子访问来使用互斥锁进行回旋的基本段错误,对于任何指出哪些神经元在我的脑中急剧失灵的人,我将深表感谢!:)

约翰·卡尔斯贝克

这是ABA问题的一种形式一个成功的制作人看起来像这样:

  1. 加载 currentReadIndex
  2. 加载 currentWriteIndex
  3. cmpxchg商店 scratchIndex = nextWriteIndex
  4. 商店 element
  5. 商店 writeIndex = nextWriteIndex

如果某个生产者由于某种原因在步骤2和3之间停顿了足够长的时间,则其他生产者有可能产生整个队列中的数据,然后回绕到完全相同的索引,以便步骤3中的compare-exchange成功(因为scratchIndex恰好又等于currentWriteIndex)。

就其本身而言,这不是问题。停滞不前的生产者完全有权利增加scratchIndex锁定队列的权限-即使检测到ABA的不可思议的cmpxchg拒绝了商店,生产者也将再次尝试,重新加载完全相同currentWriteIndex,然后正常进行。

实际的问题是nextWriteIndex == currentReadIndex步骤2和步骤3之间检查。如果currentReadIndex == currentWriteIndex则队列在逻辑上为空,因此存在此检查是为了确保没有生产者走得那么远,以至于它覆盖了尚未出现使用者的元素。在顶部进行一次此检查似乎是安全的,因为应将所有消费者“困在”被观察者currentReadIndex与被观察者之间currentWriteIndex

除了其他生产者可以出现并破坏之外writeIndex,这使消费者摆脱了陷阱。如果生产者在第2步和第3步之间停滞不前,当它唤醒时,的存储值readIndex绝对可以是任何东西。

这是一个示例,从一个空队列开始,显示了发生的问题:

  1. 生产者A运行步骤1和2。两个已加载的索引均为0。队列为空。
  2. 生产者B中断并产生一个元素。
  3. 消费者弹出一个元素。两个索引均为1。
  4. 生产者B再生产255个元素。写索引回绕为0,读索引仍为1。
  5. 生产者A从沉睡中醒来。它先前已将读取和写入索引都加载为0(空队列!),因此它尝试执行步骤3。因为另一个生产者恰巧在索引0上暂停,所以比较交换成功,并且存储继续进行。完成时,生产者让writeIndex = 1,现在存储的两个索引均为1,并且队列在逻辑上为空。现在,将完全忽略队列中所有元素的价值。

(我应该提一下,我无法谈论“停滞”和“唤醒”的唯一原因是所使用的所有原子都是顺序一致的,因此我可以假装我们处于单线程环境中。)


请注意,您scratchIndex用来保护并发写入的方式本质上是一个锁。成功完成cmpxchg的人将获得对该队列的总写访问权限,直到释放该锁为止。解决此故障的最简单方法是scratchIndex用自旋锁代替-它不会受到ABA的影响,而这实际上正在发生。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

Erlang有界缓冲区测试程序

来自分类Dev

有界缓冲区,互斥与空的顺序。首先

来自分类Dev

如何实现有界缓冲区的同步检查以避免竞争条件?

来自分类Dev

使用Java库信号量的有界缓冲区

来自分类Dev

环形缓冲区的有效边界检查

来自分类Dev

C基本环形缓冲区问题

来自分类Dev

带时间戳的环形缓冲区

来自分类Dev

为什么我得到不同的输出?我正在使用有界缓冲区、pthreads 和信号量使用 C++ 进行编码

来自分类Dev

由于最大容量的环形缓冲区,Logstash TCPSocketAppender 丢弃所有日志 [8192]

来自分类Dev

无锁fifo缓冲区中的已删除节点检测

来自分类Dev

检测环形缓冲区的无效迭代器

来自分类Dev

UNIX上的环形缓冲区日志文件

来自分类Dev

BPF环形缓冲区无效参数(-22)?

来自分类Dev

环形缓冲区和队列之间的区别

来自分类Dev

如何找出Linux内核环形缓冲区的大小?

来自分类Dev

有界图元

来自分类Dev

为什么strstr无法从环形缓冲区中找到子字符串?

来自分类Dev

wintun:注册环形缓冲区时出现ERROR_INVALID_PARAMETER

来自分类Dev

“内核环形缓冲区”,“用户级别”,“日志级别”的概念是什么?

来自分类Dev

创建像环形缓冲区这样的批处理Observable(需要建议)

来自分类Dev

在设置绑定接口之前,系统本地管理NIC环形缓冲区大小的方法是什么?

来自分类Dev

通过线程之间的环形(循环)缓冲区发送消息(在 C 中)

来自分类Dev

我可以在没有任何锁的情况下从不同线程读取内存缓冲区吗?

来自分类Dev

带有协议缓冲区的RPC

来自分类Dev

具有Qt的C ++缓冲区

来自分类Dev

带有CMakeLists的协议缓冲区

来自分类Dev

有界累计总和?

来自分类Dev

有界子集和

来自分类Dev

返回有界通配符

Related 相关文章

  1. 1

    Erlang有界缓冲区测试程序

  2. 2

    有界缓冲区,互斥与空的顺序。首先

  3. 3

    如何实现有界缓冲区的同步检查以避免竞争条件?

  4. 4

    使用Java库信号量的有界缓冲区

  5. 5

    环形缓冲区的有效边界检查

  6. 6

    C基本环形缓冲区问题

  7. 7

    带时间戳的环形缓冲区

  8. 8

    为什么我得到不同的输出?我正在使用有界缓冲区、pthreads 和信号量使用 C++ 进行编码

  9. 9

    由于最大容量的环形缓冲区,Logstash TCPSocketAppender 丢弃所有日志 [8192]

  10. 10

    无锁fifo缓冲区中的已删除节点检测

  11. 11

    检测环形缓冲区的无效迭代器

  12. 12

    UNIX上的环形缓冲区日志文件

  13. 13

    BPF环形缓冲区无效参数(-22)?

  14. 14

    环形缓冲区和队列之间的区别

  15. 15

    如何找出Linux内核环形缓冲区的大小?

  16. 16

    有界图元

  17. 17

    为什么strstr无法从环形缓冲区中找到子字符串?

  18. 18

    wintun:注册环形缓冲区时出现ERROR_INVALID_PARAMETER

  19. 19

    “内核环形缓冲区”,“用户级别”,“日志级别”的概念是什么?

  20. 20

    创建像环形缓冲区这样的批处理Observable(需要建议)

  21. 21

    在设置绑定接口之前,系统本地管理NIC环形缓冲区大小的方法是什么?

  22. 22

    通过线程之间的环形(循环)缓冲区发送消息(在 C 中)

  23. 23

    我可以在没有任何锁的情况下从不同线程读取内存缓冲区吗?

  24. 24

    带有协议缓冲区的RPC

  25. 25

    具有Qt的C ++缓冲区

  26. 26

    带有CMakeLists的协议缓冲区

  27. 27

    有界累计总和?

  28. 28

    有界子集和

  29. 29

    返回有界通配符

热门标签

归档