我一直在一个无锁的多个生产者多个消费者环形缓冲区上反对(我的尝试)。这个想法的基础是使用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;
}
}
}
};
写作的主要思想是使用临时索引&#39; scratchIndex&#39;在更新writeIndex并允许任何其他生产者取得进展之前,它执行伪锁定以允许任何一次只有一个生产者复制构造到元素缓冲区。在我被称为异教徒之前暗示我的方法是无锁定的#39;我意识到这种方法并非完全无锁,但在实践中(如果它可以工作!)它明显快于拥有普通的互斥锁!
我知道一个(更复杂的)MPMC环形缓冲解决方案http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue,但我正在尝试我的想法,然后与这种方法进行比较,找出每个方法的优势(或者实际上我的方法是否平坦失败!)。
我尝试过的事情;
我确信这可以归结为我脑子里的一个基本的断句,即如何使用原子访问来使用互斥量,我会完全感谢能指出哪些神经元严重失误的人在我的脑子里! :)
答案 0 :(得分:2)
这是A-B-A problem的一种形式。一个成功的制作人看起来像这样:
currentReadIndex
currentWriteIndex
scratchIndex = nextWriteIndex
element
writeIndex = nextWriteIndex
如果生产者在步骤2和3之间由于某种原因拖延了足够长的时间,那么其他生产者可能会生成一个完整队列的数据并回绕到完全相同的索引,以便步骤3中的compare-exchange成功(因为scratchIndex恰好等于currentWriteIndex)。
就其本身而言,这不是一个问题。停滞不前的生产者完全有权增加scratchIndex
来锁定队列 - 即使一个神奇的ABA检测cmpxchg拒绝了商店,生产者只会再次尝试,重新加载完全相同的currentWriteIndex
,并且正常进行。
实际问题是步骤2和3之间的nextWriteIndex == currentReadIndex
检查。如果currentReadIndex == currentWriteIndex
,队列在逻辑上是空的,所以这个检查是存在的,以确保没有生产者如此先于它覆盖元素没有消费者已经弹出。在顶部进行一次检查似乎是安全的,因为所有的消费者都应该被困在#34;在观察到的currentReadIndex
和观察到的currentWriteIndex
之间。
除了另一个制作人可以出现并提升writeIndex
,这将使消费者摆脱陷阱。如果制作人在第2步和第3步之间停顿,当它被唤醒时,readIndex
的存储值绝对可能是任何东西。
这是一个以空队列开头的示例,它显示了发生的问题:
writeIndex = 1
,现在两个存储的索引都是1,并且队列在逻辑上是空的。现在将完全忽略完整队列的元素。(我应该提一下,我可以谈论&#34;拖延&#34;以及&#34;醒来&#34;唯一的原因是所使用的所有原子都是顺序一致的,所以我可以假装我们处于单线程环境中。)
请注意,使用scratchIndex
来保护并发写入的方式本质上是一个锁;无论谁成功完成cmpxchg,都会获得对队列的总写入权限,直到它释放锁定为止。解决此失败的最简单方法是用螺旋锁替换scratchIndex
- 它不会受到A-B-A的影响,而且它实际上发生了什么。
答案 1 :(得分:1)
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;
}
}
}
我已经标出了有问题的地方。多个线程可以同时到达writeIndex = nextWriteIndex。数据将以任何顺序写入,尽管每次写入都是原子的。
这是一个问题,因为您尝试使用相同的原子条件更新两个值,这通常是不可能的。假设你的方法的其余部分没问题,那么解决这个问题的方法是将scratchIndex和writeIndex合并为一个double-size值。例如,将两个uint32_t值作为单个uint64_t值处理并在其上进行原子操作。