Multithreading 无锁有界MPMC环缓冲区故障
我一直在用头撞击(我的尝试)一个无锁的多生产者多消费者环形缓冲区。该思想的基础是使用无符号char和无符号short类型的固有溢出,将元素缓冲区修复为这两种类型中的任何一种,然后有一个返回到环形缓冲区开头的自由循环 问题是-我的解决方案不适用于多个生产者(虽然它适用于N个消费者,也适用于单个生产者和单个消费者)Multithreading 无锁有界MPMC环缓冲区故障,multithreading,c++11,atomic,lockless,Multithreading,C++11,Atomic,Lockless,我一直在用头撞击(我的尝试)一个无锁的多生产者多消费者环形缓冲区。该思想的基础是使用无符号char和无符号short类型的固有溢出,将元素缓冲区修复为这两种类型中的任何一种,然后有一个返回到环形缓冲区开头的自由循环 问题是-我的解决方案不适用于多个生产者(虽然它适用于N个消费者,也适用于单个生产者和单个消费者) #包括,但我确实在尝试我的想法,然后与该方法进行比较,找出每种方法的优点(或者我的方法是否完全失败!) 我尝试过的事情 使用比较交换弱 使用更精确的std::memory\u顺序来匹配
#包括,但我确实在尝试我的想法,然后与该方法进行比较,找出每种方法的优点(或者我的方法是否完全失败!)
我尝试过的事情
- 使用比较交换弱
- 使用更精确的std::memory\u顺序来匹配我想要的行为
- 在我拥有的各种索引之间添加缓存线填充
- 使元素std::原子化而不仅仅是元素数组
我确信这可以归结为我头脑中的一个根本错误,即如何使用原子访问来绕过互斥,我将非常感谢任何人能够指出哪些神经元在我头脑中严重失火!:)
我已经标出了有问题的地方。多个线程可以同时访问writeIndex=nextWriteIndex。数据将以任何顺序写入,尽管每次写入都是原子的
这是一个问题,因为您试图使用相同的原子条件更新两个值,这通常是不可能的。假设方法的其余部分没有问题,一种解决方法是将scratchIndex和writeIndex合并为一个大小为两倍的值。例如,将两个uint32_t值视为单个uint64_t值,并在其上进行原子操作。这是。成功的制作人看起来是这样的:
加载currentReadIndex
加载currentWriteIndex
cmpxchg存储scratchIndex=nextWriteIndex
存储元素
存储writeIndex=nextWriteIndex
如果某个生产者由于某种原因在步骤2和步骤3之间暂停足够长的时间,则其他生产者可能会生成整个队列的数据,并返回到完全相同的索引,以便步骤3中的比较交换成功(因为scratchIndex恰好再次等于currentWriteIndex)
这本身不是问题。暂停的生产者完全有权增加scratchIndex
以锁定队列,即使神奇的ABA检测到cmpxchg拒绝存储,生产者只需重试,重新加载完全相同的currentWriteIndex
,然后正常进行
实际问题是步骤2和步骤3之间的nextWriteIndex==currentReadIndex
检查。如果currentReadIndex==currentWriteIndex
,则队列在逻辑上是空的,因此此检查的存在是为了确保没有生产者领先太远,以至于它覆盖了还没有消费者弹出的元素。在顶部执行一次此检查似乎是安全的,因为所有使用者都应该被“困”在观察到的currentReadIndex
和观察到的currentWriteIndex
之间
除了另一个制作人可以出现并启动writeIndex
,从而将消费者从陷阱中解放出来。如果生产者在第2步和第3步之间暂停,当它唤醒时,readIndex
的存储值可能是任何值
下面是一个示例,从一个空队列开始,它显示了发生的问题:
生产者A运行步骤1和2。两个加载的索引都是0。队列是空的
生产者B中断并产生一个元素
消费者弹出一个元素。两项指数均为1
生产者B再产生255个元素。写索引变为0,读索引仍然为1
制作人A从沉睡中醒来。它以前将读和写索引都加载为0(空队列!),因此它尝试执行步骤3。因为另一个生产者碰巧在索引0上暂停,所以比较交换成功,存储继续进行。完成时,生产者让writeIndex=1,现在两个存储的索引都是1,队列逻辑上是空的。一个完整队列的元素值现在将被完全忽略
(我应该提到的是,我可以不谈论“暂停”和“醒来”的唯一原因是使用的所有原子都是顺序一致的,因此我可以假装我们处于单线程环境中。)
请注意,您使用scratchIndex
保护并发写入的方式本质上是一种锁;成功完成cmpxchg的人将获得对队列的总写访问权,直到队列释放锁为止。修复此故障的最简单方法是用自旋锁替换scratchIndex
,它不会受到a-B-a的影响,这就是实际发生的情况。那么,问题到底是什么?你想知道为什么这不起作用,或者如何使它起作用,或者…?我不知道为什么它不适用于多个生产者的情况!
#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;
}
}
}
};
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;
// Problem here!
writeIndex = nextWriteIndex;
return true;
}
}
}