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



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

因此,我偶然在 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_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才能按预期工作,这就是我几天前提交另一个拉取请求的原因。或者,他们可以将采集负载从循环中取出,并在最后使用围栏:

//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在这里都用错了。

最新更新