无锁单生产者/单消费者循环缓冲区

时间:2019-01-19 14:47:40

标签: c++ multithreading c++11 atomic lock-free

我一直在this website上查看无锁的单个生产者/单个使用者循环缓冲区,但当时我无法弄清为什么需要特定的内存屏障。 我已经仔细阅读了the standard rules about memory order一遍,但是我不明白我缺少什么。

通过此实现,只有一个唯一的线程可以调用push()函数,而另一个唯一的线程可以调用pop()函数。

这是Producer代码:

bool push(const Element& item)
{       
  const auto current_tail = _tail.load(std::memory_order_relaxed);  //(1)
  const auto next_tail = increment(current_tail);

  if(next_tail != _head.load(std::memory_order_acquire))            //(2)               
  {     
    _array[current_tail] = item;                                    //(3)
    _tail.store(next_tail, std::memory_order_release);              //(4)
    return true;
  }
  return false; // full queue
}

这是Consumer代码:

bool pop(Element& item)
{
  const auto current_head = _head.load(std::memory_order_relaxed);    //(1)
  if(current_head == _tail.load(std::memory_order_acquire))           //(2)
    return false; // empty queue

  item = _array[current_head];                                       //(3)
  _head.store(increment(current_head), std::memory_order_release);   //(4)
  return true;
}

我理解为什么绝对需要Producer (4)Consumer (2)语句,这是因为我们必须确保所有在{{1}之前发生的写入操作一旦(4) released store看到存储的值,Producer}的副作用将是可见的。

我也理解为什么需要consumer语句,这是为了确保在执行Consumer (4)存储之前执行Consumer (3)加载。

问题

  • 为什么Consumer (4)加载需要使用 acquire语义(而不是 relaxed )来执行?是否可以防止在条件(编译时或运行时)之前重新Producer (2)

2 个答案:

答案 0 :(得分:2)

我们需要证明

_array[current_tail] = item; // push(3)

在符合性(current_head == current_tail之后执行

item = _array[current_head]; // pop(3)

完成。我们只能在单元格中的数据已经复制到项目之后覆盖单元格

_head.load(std::memory_order_acquire) // push(2)

同步
_head.store(increment(current_head), std::memory_order_release);   //pop(4)

通过发布-获取顺序

_head上原子存储释放( pop(4))之前发生的

所有内存写入( pop(3))变为可见-在_head上完成原子负载获取( push(2))后生效。

因此在 push(2)完成后的生产者代码,保证可以看到 pop(3)的结果。这意味着将_array[current_head]中的数据复制到项目,并且在 push(2)之后,生产者代码可以看到此操作的结果,因此_array[current_head]已经可用。

memory_order_acquire加载描述的另一面

-在此加载之前,当前线程中的任何读写( push(3))都不能重新排序。因此 push(3)将在 push(2)加载完成后执行,但此时 pop(3)已经完成

item = _array[current_head];                                        //pop(3)
_head.store(increment(current_head), std::memory_order_release);    //pop(4)
-----
    _head.load(std::memory_order_acquire);                          //push(2)
    _array[current_tail] = item;                                    //push(3)         

答案 1 :(得分:1)

内存屏障可防止CPU在对队列结构的访问(此处使用索引实现,但指针同样可行)中对未使用互锁的Element对象的访问进行重新排序。

使用编号,至关重要的是在(2)和(4)之间执行(3),并通过内存屏障来实现。

在生产者中询问有关(2)-vs-(3)的确切情况,可以防止在队列已满(建议的站点与有效数据重叠)时推测性地覆盖有效数据。没有障碍,即使当条件失败时,从Producer线程的角度来看,原始数据将被恢复,消费者可能会短暂看到中间值。