C中的任何单消费者单生产者无锁队列实现?

Posted

技术标签:

【中文标题】C中的任何单消费者单生产者无锁队列实现?【英文标题】:Any single-consumer single-producer lock free queue implementation in C? 【发布时间】:2009-06-13 12:57:27 【问题描述】:

我正在写一个带有消费者线程和生产者线程的程序,现在看来队列同步在程序中是一个很大的开销,我寻找了一些无锁队列的实现,但只找到了Lamport的版本和改进的版本在 PPPoPP '08:

enqueue_nonblock(data) 
    if (NULL != buffer[head]) 
        return EWOULDBLOCK;
    
    buffer[head] = data;
    head = NEXT(head);
    return 0;


dequeue_nonblock(data) 
    data = buffer[tail];
    if (NULL == data) 
        return EWOULDBLOCK;
    
    buffer[tail] = NULL;
    tail = NEXT(tail);
    return 0;

两个版本都需要为数据预先分配数组,我的问题是有没有使用 malloc() 动态分配空间的单消费者单生产者无锁队列实现?

另一个相关的问题是,如何准确测量队列同步的开销?比如pthread_mutex_lock()需要多少时间等

【问题讨论】:

【参考方案1】:

如果您担心性能,将 malloc() 添加到组合中将无济于事。如果您不担心性能,为什么不简单地通过互斥锁控制对队列的访问。您是否实际测量过这种实现的性能?在我看来,您似乎正在走熟悉的过早优化路线。

【讨论】:

我同意你的 malloc 点,但不同意互斥锁。锁死。所以一个生产者和一个消费者无锁工作,一个应该使用它。现在这个消费者稍后可以应用分片逻辑将数据扔给不同的消费者。 LOCK 杀死。【参考方案2】:

您展示的算法能够正常工作,因为尽管两个线程共享资源(即队列),但它们以一种非常特殊的方式共享它。因为只有一个线程改变了队列的头索引(生产者),并且每个线程都改变了尾索引(当然是消费者),所以您无法获得共享对象的不一致状态。同样重要的是,生产者在更新头索引之前将实际数据放入,而消费者在更新尾索引之前读取它想要的数据。

它的工作原理和 b/c 一样好,数组是相当静态的;两个线程都可以依靠存储的元素。您可能无法完全替换数组,但您可以做的就是更改数组的用途。

也就是说,不是将数据保存在数组中,而是使用它来保存指向数据的指针。然后您可以 malloc() 和 free() 数据项,同时通过数组在线程之间传递对它们的引用(指针)。

此外,posix 确实支持读取纳秒时钟,尽管实际精度取决于系统。您可以在前后读取这个高分辨率时钟,然后减去。

【讨论】:

该算法确定需要添加一些内存屏障吗? 是的。他说“生产者在更新头部索引之前放入实际数据,消费者在更新尾部索引之前读取它想要的数据也很重要。”跨度> @bdonlan:(等)不是这样。它完全取决于操作顺序和单个生产者、单个消费者的事实。在这种情况下,这很好。 仅在写入未重新排序的机器上(这都是 AFAIK),更重要的是在写入未在读取之前移动的机器上。请注意,编译器更改不如 CPU 重新排序重要。 @ben 是正确的:在弱排序机器上,缓存未命中或缓存行争用会导致存储延迟 很长 时间,从而允许大量其他操作在它之前变得全局可见。根据需要使用 C11 标准原子发布存储来防止编译时重新排序(即使在 x86 上),以及在其他平台上编译 + 运行时重新排序。【参考方案3】:

是的。

存在多个无锁多读多写队列。

Michael 和 Scott 从他们 1996 年的论文中实现了一个。

我将(经过更多测试后)发布一个小型无锁数据结构库(C 语言),其中将包含此队列。

【讨论】:

1.这些使用 malloc 节点,这往往会降低性能 2. 该算法使用 CAS - CAS 将 LOCK 放在内存上,因此不如上述算法。事实上,在很少持有锁的情况下(例如快速锁),那么多核上的 CAS == SpinLock。不过还是想看看。【参考方案4】:

你应该看看 FastFlow 库

【讨论】:

【参考方案5】:

我记得几年前看过一个看起来很有趣的东西,但现在我似乎找不到了。 :( 提议的无锁实现确实需要使用CAS primitive,尽管即使锁定实现(如果您不想使用 CAS 原语)也具有相当好的性能特征——锁只能防止多个读者或多个生产者同时进入队列,生产者仍然从未与消费者竞争。

我确实记得队列背后的基本概念是创建一个链表,其中总是有一个额外的“空”节点。这个额外的节点意味着列表的头和尾指针只会在列表为空时引用相同的数据。我希望我能找到这篇论文,我的解释并不是在做算法正义......

啊哈!

我发现有人抄写了the algorithm without the remainder of the article。这可能是一个有用的起点。

【讨论】:

最值得注意的是阅读该 URL 中的细则(搜索“powerpc”),并在您开始发明自己的无锁结构时牢记这一点。 你给出的描述是迈克尔和斯科特的作品——我从上面评论中的链接中看到确实是这部作品;那里的伪代码直接取自论文。虚拟节点的想法实际上来自 Valois。【参考方案6】:

我使用了一个相当简单的队列实现,它符合您的大部分标准。它使用了一个静态最大大小的字节池,然后我们在其中实现了消息。有一个进程会移动的头指针和另一个进程会移动的尾指针。

仍然需要锁,但我们使用了Peterson's 2-Processor Algorithm,它非常轻量级,因为它不涉及系统调用。锁只在非常小的、有界的区域才需要:最多几个 CPU 周期,所以你永远不会长时间阻塞。

【讨论】:

【参考方案7】:

我认为分配器可能是一个性能问题。您可以尝试使用自定义多线程内存分配器,该分配器使用链表来维护释放的块。如果你的块不是(几乎)相同的大小,你可以实现一个“好友系统内存分配器”,女巫非常快。您必须将队列(环形缓冲区)与互斥锁同步。

为避免过多的同步,您可以尝试在每次访问时向队列写入/读取多个值。

如果您仍想使用无锁算法,那么您必须使用预分配数据或使用无锁分配器。 有一篇关于无锁分配器“Scalable Lock-Free Dynamic Memory Allocation”的论文,以及一个实现Streamflow

在开始使用无锁的东西之前,请查看:Circular lock-free buffer

【讨论】:

【参考方案8】:

添加 malloc 会扼杀您可能获得的任何性能提升,而基于锁的结构将同样有效。这是因为 malloc 在堆上需要某种 CAS 锁,因此某些形式的 malloc 有自己的锁,因此您可能会锁定在内存管理器中。

要使用 malloc,您需要预先分配所有节点并使用另一个队列管理它们...

请注意,您可以制作某种形式的可扩展数组,如果它展开则需要锁定。

此外,虽然互锁在 CPU 上是无锁的,但它们确实会在指令期间放置内存锁和阻塞内存,并且通常会停止流水线。

【讨论】:

或者有你分配的每线程内存池。但是你需要一种方法让消费者线程偶尔将内存还给生产者。垃圾回收系统可以让生产者分配链表节点,并使用链表作为队列。【参考方案9】:

此实现使用 C++ 的 new 和 delete,可以使用 malloc 和 free 轻松地将其移植到 C 标准库:

http://www.drdobbs.com/parallel/writing-lock-free-code-a-corrected-queue/210604448?pgno=2

【讨论】:

在生产者中使用new 和在消费者中使用delete 的链表会很糟糕,除非您的分配器具有每个 CPU 的内存池。否则newdelete 将获取全局锁并相互竞争。我很惊讶 Herb Sutter 在他的文章中没有提到这个警告。也许他最初是为垃圾收集的 C# 编写的? 啊,但是如果你看,delete 不会在消费者线程中调用。 ;) 但是感谢您指出这一点,我从未想过。 消费者函数当然会将仍然分配的对象返回给它的调用者(而不是将其复制到调用者提供的缓冲区,或者按值返回对象)。如果消费者将节点添加到它自己的数据结构中,那么它可以保持分配状态。但它需要在消耗内容后的某个时候delete 它以避免泄漏。这很可能是在对数据进行几次函数调用之后,在将数据从队列中取出后不久。 这是真的。我想作为一个模板实现,该类型很可能是指向某些先前分配的内存的指针。如果您暗示像无锁“环形缓冲区”(例如,this)可能会更好,我完全同意。

以上是关于C中的任何单消费者单生产者无锁队列实现?的主要内容,如果未能解决你的问题,请参考以下文章

并发无锁队列

并发无锁队列学习(单生产者单消费者模型)

单生产者/单消费者 的 FIFO 无锁队列

dpdk无锁队列rte_ring实现分析

dpdk无锁队列rte_ring实现分析

C ++ 11中无锁的多生产者/消费者队列