在 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的主要内容,如果未能解决你的问题,请参考以下文章
为啥在已经使用 seq_cst CAS 的无锁队列中需要 atomic_thread_fence(memory_order_seq_cst)?