在试图理解如何处理无锁代码的过程中,我尝试编写一个单一的消费者/单一生产者无锁队列。与往常一样,我检查了论文、文章和代码,特别是考虑到这是一个有点微妙的主题。
因此,我偶然在 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
在这里都用错了。