在 Folly 的无锁 SPSC 队列中使用 std::memory_order_consume

Posted

技术标签:

【中文标题】在 Folly 的无锁 SPSC 队列中使用 std::memory_order_consume【英文标题】:Use of std::memory_order_consume in the Folly's lock free SPSC queue 【发布时间】:2016-03-22 15:57:03 【问题描述】:

在尝试了解如何处理无锁代码的过程中,我尝试编写一个单一消费者/单一生产者无锁队列。和往常一样,我检查了论文、文章和代码,特别是考虑到这是一个有点微妙的主题。

所以,我在 Folly 库中偶然发现了这种数据结构的实现,可以在这里找到: https://github.com/facebook/folly/blob/master/folly/ProducerConsumerQueue.h

正如我看到的每个无锁队列,这个似乎使用循环缓冲区,所以我们有两个std::atomic<unsigned int> 变量:readIndex_writeIndex_readIndex_ 表示我们将读取的下一个索引,writeIndex_ 表示我们将写入的下一个索引。看起来很简单。

所以,乍一看,这个实现看起来很干净而且很简单,但我发现有一点很麻烦。事实上,isEmpty()isFull()guessSize() 等一些函数正在使用 std::memory_order_consume 来检索索引的值。

老实说,我真的不知道它们的用途是什么。不要误会我的意思,我知道 std::memory_order_consume 在通过原子指针进行依赖的经典案例中的使用,但是在这里,我们似乎没有携带任何依赖!我们只是得到索引,无符号整数,我们不创建依赖项。在这种情况下,对我来说,std::memory_order_relaxed 是等价的。

但是,我不相信自己比那些设计此代码的人更了解内存排序,因此我在这里问这个问题。有什么我遗漏或误解的吗?

提前感谢您的回答!

【问题讨论】:

很少有std::memory_order_relaxed 真正发挥作用的情况。你真的需要一个能够撕裂读/写的芯片。一个例子是 x86 在处理未对齐的数据时,但你不应该这样做。我之所以这么说,是因为如果您认为需要memory_order_relaxed,您可能不了解其用法。您是否试图防止读取/写入撕裂? @SideEffects:我同意你的看法。没有后续的内存访问依赖于这些函数中的索引值,所以我看不出std::memory_order_consume 怎么可能贡献任何有用的东西。作者的电子邮件地址位于文件顶部;也许尝试给他们发电子邮件? (如果你这样做,请在此处添加更新或答案。我很好奇我们是否遗漏了什么。) @SergeyA 是的,我在这里可能有点不清楚。我想说的是,我相信消耗内存排序至少对我来说没有添加任何有用的东西。但是,是的,我可能想澄清这一点,谢谢! @Nemo 在给他们发电子邮件之前,我会稍等片刻,以便在继续之前尝试获得尽可能多的反馈。感谢您的评论! 【参考方案1】:

几个月前我也有同样的想法,所以我在 10 月份提交了 this pull request,建议他们将 std::memory_order_consume 加载更改为 std::memory_order_relaxed,因为消耗根本没有意义,因为没有依赖项可以使用这些函数从一个线程传送到另一个线程。它最终引发了一些讨论,揭示了isEmpty()isFull()sizeGuess 的可能用例如下:

//Consumer    
while( queue.isEmpty() )  // spin until producer writes 
use_queue(); // At this point, the writes from producer _should_ be visible

这就是为什么他们解释说std::memory_order_relaxed 不合适而std::memory_order_consume 合适。然而,这只是因为std::memory_order_consume 在我知道的所有编译器上都被提升为std::memory_order_acquire。因此,尽管std::memory_order_consume 似乎提供了正确的同步,但将其留在代码中并假设它会保持正确是非常误导的,特别是如果std::memory_order_consume 要按预期实现。上述用例将无法在较弱的架构上工作,因为不会生成适当的同步。

他们真正需要的是让这些负载std::memory_order_acquire 使其按预期工作,这就是我几天前提交this other pull request 的原因。或者,他们可以将获取负载从循环中取出并在最后使用围栏:

//Consumer    
while( queue.isEmpty() )  // spin until producer writes using relaxed loads
std::atomic_thread_fence(std::memory_order_acquire);
use_queue(); // At this point, the writes from producer _should_ be visible

不管怎样,std::memory_order_consume 在这里使用不正确。

【讨论】:

感谢您花时间回答!巧合的是,我正要给他们发一封电子邮件,就在你回答之前。因此,即使实现与标准定义的不正确,它也更有意义。对我来说,'isEmpty()' 方法仍然是一个非常奇怪的保证。 我同意,consume 这种函数的语义似乎不合适。 Boost 甚至具有使用 relaxed 的等效功能。我很高兴其他人也发现了它,我知道我不可能是唯一的!

以上是关于在 Folly 的无锁 SPSC 队列中使用 std::memory_order_consume的主要内容,如果未能解决你的问题,请参考以下文章

folly无锁队列正确性说明

基于共享内存的无锁消息队列设计

为啥在已经使用 seq_cst CAS 的无锁队列中需要 atomic_thread_fence(memory_order_seq_cst)?

是否存在多个读取或写入线程的无锁队列?

是否存在乐观的无锁FIFO队列实现?

谈谈存储软件的无锁设计