圆形无锁缓冲器


73

我正在设计一个系统,该系统连接到一个或多个数据提要流,并根据触发结果对数据进行一些分析。在典型的多线程生产者/消费者设置中,我将有多个生产者线程将数据放入队列中,并且有多个消费者线程读取数据,并且消费者只对最新数据点加n个点感兴趣。如果慢速使用者无法跟上生产者线程,则生产者线程将不得不阻塞,当然,当没有未处理的更新时,使用者线程也会阻塞。使用具有读取器/写入器锁定的典型并发队列会很好地工作,但是数据输入的速率可能很大,因此我想减少我的锁定开销,特别是为生产者减少写入器锁定。我认为我需要一个循环无锁缓冲区。

现在有两个问题:

  1. 循环无锁缓冲区是否是答案?

  2. 如果是这样,在我提出自己的建议之前,您知道任何适合我需要的公共实施方式吗?

始终欢迎实现循环无锁缓冲区的任何指针。

顺便说一句,在Linux上的C ++中执行此操作。

一些其他信息:

响应时间对我的系统至关重要。理想情况下,使用者线程希望尽快看到任何更新,因为额外的1毫秒延迟可能会使系统一文不值,甚至少很多。

我倾向于的设计思想是一个无半锁的循环缓冲区,在此缓冲区中,生产者线程将数据尽可能快地放入缓冲区中,我们将其称为缓冲区A的头,除非缓冲区已满,否则不要阻塞A与缓冲区Z的末尾相遇。使用者线程将分别持有两个指向循环缓冲区的指针P和P n,其中P是线程的本地缓冲区头,而P n是P之后的第n个项目。每个使用者线程都将推进其P一旦完成当前P的处理,P n和P n就会以最慢的P n前进到缓冲区指针Z的末尾。当P赶上A,这意味着不再有新的更新要处理时,使用者旋转并忙于等待A再次前进。如果使用者线程旋转时间过长,则可以使其进入睡眠状态并等待条件变量,但是我可以接受使用者占用CPU周期等待更新,因为这不会增加我的延迟(我将拥有更多的CPU内核)比线程)。想象一下,您有一个循环的轨道,而生产者在一群消费者的面前运行,关键是调整系统,以便生产者通常比消费者领先几步,而其中大多数操作可以使用无锁技术完成。我知道正确实现实现的细节并不容易...好吧,非常艰苦,这就是为什么我想在别人做些自己的事情之前先从别人的错误中学习。


我认为,如果您要草绘要实现此数据结构的API,那将很有帮助。
戴夫

1
我学到的东西需要大量工作。我不知道您的工作项的大小,但是如果您可以生产更大的块并消耗更大的块,则可以提高效率。您还可以通过使用可变大小的块来增加它,这样使用者就不会立即全部完成并争夺数据队列。
Zan Lynx

需要考虑的另一件事是,您是否需要一个或一系列缓冲区。您可以让生产者/消费者对共享一个缓冲区,并且当一个缓冲区已满时,生产者或使用者临时切换到另一个打开的缓冲区。这是偷工的一种形式。
Zan Lynx

3
高效的无锁算法是独特的雪花,其发现通常值得研究。在OP将他的实际需求与他认为解决方案的外观区别开来之前,我不会尝试回答这个问题。
戴夫

2
在未经修改的Linux上,毫秒级是非常快速的期限。如果另一个进程开始运行,那么您很容易错过它。您将需要使用实时优先级,即使如此,我仍不确定您能否可靠地满足这些期限。您确定需要做出快速反应吗?您能否仅使生产者加快生产速度,在例如设备驱动程序中实施它们,并放宽对消费者的要求?
道格

Answers:


41

在最近几年中,我对无锁数据结构进行了专门研究。我已经阅读了该领域的大多数论文(大约只有40篇,尽管只有10篇或15篇是真正的用途:-)

AFAIK,尚未发明无锁循环缓冲区。问题将是处理读者超越作家或反之亦然的复杂情况。

如果您至少有六个月没有学习无锁数据结构,请不要尝试自己编写一个。您将弄错了,直到您的代码在新平台上部署失败之后,错误才会对您显而易见。

我相信,但是您的要求可以解决。

您应该将无锁队列与无锁空闲列表配对。

空闲列表将为您提供预分配,从而消除了对无锁分配器的(非常昂贵的)需求;当空闲列表为空时,您可以通过立即从队列中将元素出队并使用该元素来复制循环缓冲区的行为。

(当然,在基于锁的循环缓冲区中,一旦获得了锁,获取元素就非常快-基本上只是指针取消引用-但在任何无锁算法中都不会得到它;它们通常必须走这完全不符合他们的做事方式;使自由列表弹出失败然后出队的开销与任何无锁算法都需要完成的工作量相当)。

迈克尔和斯科特在1996年开发了一个非常好的无锁队列。下面的链接将为您提供足够的详细信息,以查找其论文的PDF。迈克尔和斯科特(FIFO)

无锁的自由列表是最简单的无锁算法,实际上我认为我没有看到它的实际论文。


@空白Xavier:Michael和Scott的FIFO看起来很像我在.net中独立实现的;看起来并不难。如果.net运行时和垃圾收集器不能保证在存在引用时对象永远不会被回收,那么很难防止ABA问题(上面链接的Michael和Scott论文没有提及),但是.net垃圾收集器自动解决了该问题。出于好奇,您如何解决ABA问题?
超级猫

@Supercat:M&S论文通过使用指针-计数器对显式解决了ABA问题;“结构pointer_t {ptr:指向node_t的指针,计数:无符号整数}”。计数器在必要时增加,因此几乎不可能发生ABA。

1
@空白泽维尔:啊,我莫名其妙地错过了。他们的CAS似乎假设存在交换两个项目结构的指令。我无法使用.net中的任何此类功能。
超级猫

3
我可能是错的,但我认为除非volatile造成内存障碍,否则它不会对编译器或CPU的重新排序行为产生任何影响。它要做的就是确保从内存而不是从寄存器或缓存中读取其值。

1
IIRC,volatile内存访问不会与其他volatile访问重新排序。但是在ISO C中,这就是您所得到的。在MSVC,volatile远远偏离这一点,但这些天,你应该只使用std::atomicmemory_order_releaseseq_cst你想或什么的。
彼得·科德斯

34

您想要的艺术术语是无锁队列。罗斯本西纳(Ross Bencina)提供了一组出色的笔记,其中包含指向代码和论文的链接。我最信任的作品是莫里斯·赫利希Maurice Herlihy)(对美国人来说,他的名字叫“莫里斯”)。


2
队列不是循环缓冲区。

27
@空白Xavier:否,但是循环缓冲区是一个队列。问题要求排队。队列的最有效实现是...(等待它)循环缓冲区。无论如何,如果要搜索,都将搜索“无锁队列”,而不是“无锁循环缓冲区”。
诺曼·拉姆齐

1
我看不出为什么不使用信号量的任何原因?没有锁定/阻塞,消费者仅在缓冲区为空时进入睡眠状态,生产者仅在缓冲区已满时进入睡眠状态。怎么了 某个无锁队列如何比这更好呢?
TMS

6
@Tomas:无锁队列会更好,因为没有单个锁可以充当性能瓶颈。OP特别关注在争用非常高的情况下减少锁定开销。信号量无助于争用。无锁队列可以。
诺曼·拉姆齐

1
liblfds.org具有广受好评的多生产者/多消费者循环缓冲区队列。从技术上讲, 它不是无锁的:在添加条目的过程中处于休眠状态的生产者可以阻止消费者看到其他生产者的任何东西。有关其进度保证的分析,请参见stackoverflow.com/questions/45907210/…。这是非常低的争夺,并且可能是你想要的东西在实践中。(制片人,如果它不是空或满消费者之间无)
彼得·科德斯

12

如果缓冲区为空或已满,生产者或使用者将阻塞的要求建议您应使用带有信号量或条件变量的普通锁定数据结构,使生产者或使用者在数据可用之前阻塞。无锁代码通常不会在这种情况下阻塞-它旋转或放弃无法完成的操作,而不是使用OS进行阻塞。(如果您有足够的时间等到另一个线程产生或使用数据,那么为什么还要在锁上等待另一个线程完成更新数据结构的情况更糟?)

在(x86 / x64)Linux上,如果没有争用,则使用互斥锁的线程内同步相当便宜。专注于减少生产者和消费者需要抓住锁的时间。鉴于您已经说过您只关心最后记录的N个数据点,我认为循环缓冲区可以做到这一点。但是,我并不真正了解这与阻塞要求以及消费者实际使用(删除)他们读取的数据的想法是否相符。(您是否希望消费者仅查看最后N个数据点,而不是删除它们?您是否希望生产者不在乎消费者不能跟上,而只是覆盖旧数据?)

另外,正如Zan Lynx所评论的那样,当有大量数据输入时,您可以将数据聚合/缓冲成更大的块。您可以缓冲固定数量的点或在一定时间内接收到的所有数据。这意味着将减少同步操作。虽然确实会引入延迟,但是如果您不使用实时Linux,则无论如何都必须在一定程度上处理延迟。


完全同意第一段。在这里看不到任何不使用信号灯的原因。
TMS

1
@TMS,具有专用生产者线程和专用使用者线程(例如,如OP所述)的应用程序永远不能称为“无锁”。在具有N个线程的无锁应用程序中,您应该能够无限期地挂起(N-1)个或更少线程的任何组合,并且该应用程序仍应继续取得进展。但是,如果所有生产者都被挂起,那么专用的消费者线程将无法无限期地取得进展,并且如果不允许将“产品”放在地板上,那么如果不允许任何消费者运行,那么生产者将无法取得进展。
所罗门慢传

@jameslarge:“无锁”可以描述算法或数据结构(如队列)。 en.wikipedia.org/wiki/Non-blocking_algorithm通常不会应用于整个应用程序。挂起所有生产者线程仅意味着队列将为空。但是在无锁队列中,随时挂起任何一个或多个线程都不能阻止其他线程入队和出队。(即使这不容易实现;有效的实现通常也会有一个线程“声明”一个插槽:stackoverflow.com/questions/45907210/…
Peter Cordes

@Doug:是的,如果消费者/生产者发现队列为空/已满,则应该睡觉。但是,不,这并不总是意味着您应该使用传统锁来实现队列,尤其是不要对整个数据结构使用一个大锁。从技术意义上说,此队列(与先前的评论相同)不是“无锁”的,但是它确实允许生产者完全独立于消费者(无争用),从而提供了可能比获得锁更好的吞吐量。关于空->非空的有效唤醒的好处。
彼得·科德斯

6

boost库中的实现值得考虑。它易于使用且性能相当高。我编写了一个测试,并在四核i7笔记本电脑(8个线程)上运行了该程序,并每秒获得了约4M入队/出队操作。到目前为止尚未提及的另一种实现是位于http://moodycamel.com/blog/2014/detailed-design-of-a-lock-free-queue的MPMC队列。我已经在具有32个生产者和32个消费者的同一台笔记本电脑上对该实现进行了一些简单的测试。正如宣传的那样,增强型无锁队列更快。

正如大多数其他答案一样,无锁编程很难。大多数实现将很难检测到需要大量测试和调试才能解决的极端情况。这些通常是通过在代码中仔细放置内存屏障来解决的。您还将在许多学术文章中找到正确性的证明。我更喜欢使用蛮力工具测试这些实现。您计划在生产中使用的任何无锁算法都应使用http://research.microsoft.com/en-us/um/people/lamport/tla/tla.html之类的工具检查其正确性。



5

我不是硬件内存模型和无锁数据结构的专家,我倾向于避免在我的项目中使用它们,而我选择传统的锁数据结构。

但是我最近注意到视频: 基于环形缓冲区的无锁SPSC队列

这基于交易系统使用的称为LMAX distruptor的开源高性能Java库:LMAX Distruptor

根据上面的介绍,您可以使头部和尾部指针原子化,并原子性地检查头部从后面抓尾或反之亦然的情况。

在下面,您可以看到一个非常基本的C ++ 11实现:

// USING SEQUENTIAL MEMORY
#include<thread>
#include<atomic>
#include <cinttypes>
using namespace std;

#define RING_BUFFER_SIZE 1024  // power of 2 for efficient %
class lockless_ring_buffer_spsc
{
    public :

        lockless_ring_buffer_spsc()
        {
            write.store(0);
            read.store(0);
        }

        bool try_push(int64_t val)
        {
            const auto current_tail = write.load();
            const auto next_tail = increment(current_tail);
            if (next_tail != read.load())
            {
                buffer[current_tail] = val;
                write.store(next_tail);
                return true;
            }

            return false;  
        }

        void push(int64_t val)
        {
            while( ! try_push(val) );
            // TODO: exponential backoff / sleep
        }

        bool try_pop(int64_t* pval)
        {
            auto currentHead = read.load();

            if (currentHead == write.load())
            {
                return false;
            }

            *pval = buffer[currentHead];
            read.store(increment(currentHead));

            return true;
        }

        int64_t pop()
        {
            int64_t ret;
            while( ! try_pop(&ret) );
            // TODO: exponential backoff / sleep
            return ret;
        }

    private :
        std::atomic<int64_t> write;
        std::atomic<int64_t> read;
        static const int64_t size = RING_BUFFER_SIZE;
        int64_t buffer[RING_BUFFER_SIZE];

        int64_t increment(int n)
        {
            return (n + 1) % size;
        }
};

int main (int argc, char** argv)
{
    lockless_ring_buffer_spsc queue;

    std::thread write_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.push(i);
             }
         }  // End of lambda expression
                                                );
    std::thread read_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.pop();
             }
         }  // End of lambda expression
                                                );
    write_thread.join();
    read_thread.join();

     return 0;
}

对您使用2的幂size,所以%(模)只是按位与。另外,在您的广告位中存储序列号将减少生产者和消费者之间的争用。在这种情况下,生产者必须读取write位置,反之亦然,因此缓存行包含内核之间的那些原子变量ping-pongs。有关插槽序列号的方式,请参见stackoverflow.com/questions/45907210/…。(这是一个多生产者多消费者队列,并且可以大大简化为这样的单个生产者/消费者队列。)
Peter Cordes

1
我很确定许多加载/存储只需要memory_order_acquireor release,而不需要default seq_cst。这在x86上有很大的不同,那里的seq_cst商店需要mfence(或xchg),但是release商店只是普通的x86商店。StoreLoad障碍也是大多数其他体系结构上最昂贵的障碍。(preshing.com/20120710/...
彼得·科德斯

1
它可能会更好地把read以后buffer的客舱布局,因此它在从不同的缓存行write。因此,这两个线程将仅读取由另一个线程写入的缓存行,而不是都写入同一缓存行。同样,它们应该是size_t:没有一点,没有32位指针的64位计数器。无符号类型使模的效率更高(godbolt.org/g/HMVL5C)。甚至uint32_t对于几乎所有用途都是合理的。最好以此大小为模板,或动态分配缓冲区。
彼得·科德斯

@PeterCordes为什么以2的幂为单位的模更有效?
baye

1
@BaiyanHuang:由于计算机使用二进制整数,所以你只要保持低n位有AND。例如x % 8= x & 7,并且按位AND比便宜得多div,甚至您可以使用编译时常数除数的技巧。
彼得·科德斯

4

减少竞争的一种有用技术是将项目散列到多个队列中,并使每个使用者专用于“主题”。

对于您的消费者最近感兴趣的商品数量-您不想锁定整个队列并对其进行迭代以找到要覆盖的商品-只需发布N个元组中的商品,即所有N个最新商品即可。实现的奖励点是,生产者将在超时时阻塞整个队列(当消费者无法跟上时),从而更新其本地元组缓存-这样您就不会对数据源造成压力。


我还考虑了老板/工人线程模型,其中老板线程多播更新为工人线程的专用队列。我认为这是您前进的方向。虽然我必须给它更多,但是当我考虑它时,老板/工人似乎开销太大,因为所有工人都必须获得相同的更新。
盛业

1
不完全是-首先,我的意思是对传入流进行切片,因此并非所有线程都争夺相同的锁/队列。第二点是在生产者端进行缓存,以适应输入中的峰值,并允许缓慢的消费者不停止生产者。
Nikolai Fetissov

但是业务逻辑要求所有工作线程都知道流进的所有数据。只有一种类型的数据传入,并且每个数据点都同等重要,因此我无法真正切入数据流并在其中包含不同的数据。不同的队列。在生产者端兑现并捆绑对数据模型的更新以防止产生拥挤,这还不足以处理负载。
盛业

输入域有多大?如果它像金融世界中的市场数据那样,则您的项目数量虽然有限,但数量很大,而更新的类型却很少。工人对输入事件有反应吗,还是他们自己进行处理,仅在必要时轮询您的输入?
尼古拉·费蒂索夫 Nikolai Fetissov)'2009年

就像金融界的市场数据一样。工作者进行自己的处理,并且在需要时可以随机访问n个更新历史记录(n是可配置的数目,但在该过程的生存期内不会更改)。我想设计一个在大大小小的n上都能正常工作的系统,这样我就可以拥有一个代码库。
成业

4

我会同意本文,并建议不要使用无锁数据结构。在无锁FIFO队列相对最近的一篇文章是这样,由同一作者(S)搜索更多的文件; 还有一篇关于Chalmers的关于无锁数据结构的博士学位论文(我丢失了链接)。但是,您没有说元素有多大-无锁数据结构仅对单词大小的项目有效地起作用,因此,如果元素大于机器单词(32或64),则必须动态分配元素位)。如果动态分配元素,则将(假定的,因为您没有分析程序,并且基本上在进行过早的优化)瓶颈转移到内存分配器,因此您需要无锁的内存分配器,例如Streamflow,并将其与您的应用程序集成。


3
如果您完全预分配了元素,则无需对分配器施加压力。

4

Sutter的队列不是最理想的,他知道。多核编程的艺术是一个很好的参考,但是在存储模型上,不相信Java专家。Ross的链接不会给您确切的答案,因为它们的库在此类问题中,等等。

进行无锁编程会带来麻烦,除非您想在解决问题之前花大量时间进行显然过度设计的工作(从对问题的描述来看,这是“追求完美”的常见疯狂行为) '(在缓存一致性中)。这需要几年的时间,导致无法先解决问题,而后来又无法优化,这是一种常见疾病。


您是否会发布指向Sutter队列分析的链接?
Nikolai Fetissov

一切都在DDJ上,并且在他的博客中对其进行了介绍的人之一。.重点是,在许多情况下都不需要使用热CAS,即使使用简单的交换,您也可以每天克服这种细粒度的问题。
rama-jka toti 09年

就是这样,谢谢。但是我相信它可能还会有一些比赛。哎呀,像预期的隐含障碍或对特定或完全一致性的理解那样敏感的任何事情,都是生产中等待发生的问题。我只是不相信细节级别能解决的问题只不过是专注于应用程序级别的设计,而不是仅仅/在有条件/已确定是这样的情况下,才能解决底层的管道问题。我为所有的努力,所有的书籍表示赞赏。但即使是MS,它也只是针对触摸式主题的文章,很难为大众市场PFX人群做好。
rama-jka toti 09年

只是一个意见,总有比要做管道更重要的工作。并行的工作不仅在队列中泛滥成灾,甚至在1990年代中期DDJ线程化文章不断进行重新发明时;也就是说,从NT到后来的Solaris和Unix,都采用类似的技术或C ++的最新成果。后者,可能会采取年龄完成,仍然打的事实,没有干净的面向对象的方式来柱体P2-PRO般乱序宇宙是明智的..
拉玛- JKA托蒂

2
丹尼斯的网站已移至landenlabs.com/code/ring/ring.html-它具有无锁的环形缓冲区。
LanDenLabs

3

尽管这是一个老问题,但没有人提到DPDK的无锁环形缓冲区。这是一个高吞吐量的环形缓冲区,支持多个生产者和多个消费者。它还提供单一使用者和单一生产者模式,并且环形缓冲区在SPSC模式下无需等待。它是用C语言编写的,支持多种体系结构。

此外,它还支持批量和突发模式,可以在其中批量入队/出队物品。通过移动原子指针简单地保留空间,该设计让多个使用者或多个生产者同时写入队列。


1
它是真正无锁的吗,还是仅当生产者/消费者在索取插槽之后但在完成入队/出队之前没有入睡时?见这一分析在多生产者多消费者队列liblfds.org,这可能工作得同样。实际上,它在低争用的情况下效果很好,但是从技术上讲并不是无锁的。无论如何,还是要投票赞成,因为批量/连拍模式听起来不错。
彼得·科德斯

我同意,它不能保证终止安全,并且根据[1024cores](1024cores.net/home/lock-free-algorithms/introduction),它是一种阻塞算法,系统可能无法向前发展。但是它在SPSC模式下变得无等待。我编辑答案以反映这一点。
Saman Barghi

1
还可以看一下Dmitry对有界MPMC队列的实现:1024cores.net/home/lock-free-algorithms/queues/…。同样,这不是无锁的,而是无锁的,非常简单有效。但是,在性能方面,取决于批处理大小,在批量/突发​​模式下,DPDK的队列可以达到每秒高达数亿次操作。原子操作和顺序读/写的结合使其非常高效。
Saman Barghi

2

只是为了完整性:OtlContainers中有经过良好测试的无锁循环缓冲区,但是它是用Delphi编写的(TOmniBaseBoundedQueue是循环缓冲区,TOmniBaseBoundedStack是有界堆栈)。同一单元(TOmniBaseQueue)中也有一个无限制的队列。动态队列中描述了无界队列-正确执行最后,无锁队列中描述了有界队列(循环缓冲区)的最初实现但此后代码已更新。




1

这是我的方法:

  • 将队列映射到数组
  • 通过下一个读取和下一个写入索引保持状态
  • 保持一个空的完整位向量

插入包括使用具有增量的CAS,并在下一次写入时翻转。有了插槽后,添加您的值,然后设置与其匹配的空/满位。

删除数据需要先检查该位,然后才能测试下溢,但除此之外,该操作与写入操作相同,但使用读取索引并清除空/满位。

被警告,

  1. 我不是这些事情的专家
  2. 当我使用原子ASM操作时,它们似乎非常慢,因此,如果最终使用了不止几个操作,则使用嵌入在插入/删除功能内的锁可能会更快。从理论上讲,单个原子操作要紧随其后(很少)非原子ASM操作可能比几个原子操作所完成的相同操作要快。但是要完成这项工作,需要手动或自动内联,因此这只是ASM的一小段。

1
原子操作本身确实确实很慢。使它们有用的原因在于它们可扩展。

如果锁内的操作非常小(如ASM的5-10行),则您可能仍会继续使用锁策略,特别是如果将锁直接写入关键部分而不是作为函数调用的话。
BCS

我很困惑。对我来说至关重要的部分来说必须串行执行的代码部分。锁是确保执行序列性的机制。你能告诉我你的意思吗?

1

你可以试试 lfqueue

简单易用,圆形设计无锁

int *ret;

lfqueue_t results;

lfqueue_init(&results);

/** Wrap This scope in multithread testing **/
int_data = (int*) malloc(sizeof(int));
assert(int_data != NULL);
*int_data = i++;
/*Enqueue*/
while (lfqueue_enq(&results, int_data) != 1) ;

/*Dequeue*/
while ( (ret = lfqueue_deq(&results)) == NULL);

// printf("%d\n", *(int*) ret );
free(ret);
/** End **/

lfqueue_clear(&results);

1

在某些情况下,不需要锁定即可防止出现竞争状况,尤其是当您只有一个生产者和消费者时。

考虑LDD3中的这一段:

如果精心实现,则在没有多个生产者或使用者的情况下,循环缓冲区不需要锁定。生产者是唯一允许修改写索引及其指向的数组位置的线程。只要写者在更新写索引之前将新值存储到缓冲区中,读者将始终看到一致的视图。反过来,读取器是唯一可以访问读取索引及其指向的值的线程。小心确保两个指针不会彼此溢出,生产者和使用者可以在没有竞争条件的情况下同时访问缓冲区。


LDD3?Linux设备驱动程序3?
维克托·波勒维

0

前一段时间,我已经找到了解决这个问题的好方法。我相信它是迄今为止发现的最小的。

该存储库提供了一个示例,说明如何使用它创建N个线程(读者和作家),然后共享一个席位。

我在测试示例中做了一些基准测试,并得到以下结果(以百万次操作/秒为单位):

按缓冲区大小

通量

按线程数

在此处输入图片说明

请注意,线程数不会改变吞吐量。

我认为这是解决这个问题的最终方法。它的工作原理是令人难以置信的快速和简单。即使有数百个线程和一个位置的队列。它可用作线程之间的管道,在队列内分配空间。

该存储库具有一些用C#和pascal编写的早期版本。我正在努力使某些东西更完整地抛光,以显示其真正的力量。

我希望你们中的一些人可以验证工作或提供一些想法。或者至少可以打破它?


0

如果以缓冲区永远不会变满为前提,请考虑使用以下无锁算法:

capacity must be a power of 2
buffer = new T[capacity] ~ on different cache line
mask = capacity - 1
write_index ~ on different cache line
read_index ~ on different cache line

enqueue:
    write_i = write_index.fetch_add(1) & mask
    buffer[write_i] = element ~ release store

dequeue:
    read_i = read_index.fetch_add(1) & mask
    element
    while ((element = buffer[read_i] ~ acquire load) == NULL) {
        spin loop
    }
    buffer[read_i] = NULL ~ relaxed store
    return element
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.