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

Use of std::memory_order_consume in the Folly's lock free SPSC queue

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

所以,我在 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 是等价的。

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

预先感谢您的回答!

几个月前我也有同样的想法,所以我在 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在这里使用不正确。