我一直在尝试(我的尝试)一个无锁的多生产者多消费者环形缓冲区。这个想法的基础是使用 unsigned char 和 unsigned short 类型的固有溢出,将元素缓冲区固定为这些类型中的任何一种,然后你有一个自由循环回到环形缓冲区的开头。
问题是 - 我的解决方案不适用于多个生产者(尽管它适用于 N 个消费者,也适用于单个生产者单个消费者)。
#include <atomic>
template<typename Element, typename Index = unsigned char> struct RingBuffer
{
std::atomic<Index> readIndex;
std::atomic<Index> writeIndex;
std::atomic<Index> scratchIndex;
Element elements[1 << (sizeof(Index) * 8)];
RingBuffer() :
readIndex(0),
writeIndex(0),
scratchIndex(0)
{
;
}
bool push(const Element & element)
{
while(true)
{
const Index currentReadIndex = readIndex.load();
Index currentWriteIndex = writeIndex.load();
const Index nextWriteIndex = currentWriteIndex + 1;
if(nextWriteIndex == currentReadIndex)
{
return false;
}
if(scratchIndex.compare_exchange_strong(
currentWriteIndex, nextWriteIndex))
{
elements[currentWriteIndex] = element;
writeIndex = nextWriteIndex;
return true;
}
}
}
bool pop(Element & element)
{
Index currentReadIndex = readIndex.load();
while(true)
{
const Index currentWriteIndex = writeIndex.load();
const Index nextReadIndex = currentReadIndex + 1;
if(currentReadIndex == currentWriteIndex)
{
return false;
}
element = elements[currentReadIndex];
if(readIndex.compare_exchange_strong(
currentReadIndex, nextReadIndex))
{
return true;
}
}
}
};
写入的主要思想是使用临时索引“scratchIndex”,它充当伪锁,以在任何时候只允许一个生产者复制构造到元素缓冲区,然后更新 writeIndex 并允许任何其他生产者取得进展. 在我因暗示我的方法是“无锁”而被称为异教徒之前,我意识到这种方法并不是完全无锁的,但在实践中(如果可行的话!)它比使用普通互斥锁要快得多!
我在这里知道一个(更复杂的)MPMC ringbuffer 解决方案http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue,但我真的在尝试我的想法然后比较反对这种方法并找出每种方法的优点(或者实际上我的方法是否完全失败了!)。
我尝试过的事情;
- 使用 compare_exchange_weak
- 使用与我想要的行为匹配的更精确的 std::memory_order
- 在我拥有的各种索引之间添加缓存线垫
- 制作元素 std::atomic 而不仅仅是元素数组
我确信这归结为我脑海中的一个基本段错误,即如何使用原子访问来绕过使用互斥体,我将完全感谢谁能指出哪些神经元在我的脑海中严重失灵!:)