我一直在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)
?答案 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线程的角度来看,原始数据将被恢复,消费者可能会短暂看到中间值。