繁体   English   中英

锁定免费队列 - 单一生产者,多个消费者

[英]Lock Free Queue — Single Producer, Multiple Consumers

我正在寻找一种方法来实现支持单个生产者和多个消费者的无锁队列数据结构。 我看过Maged Michael和Michael Scott(1996)的经典方法,但他们的版本使用链表。 我想要一个使用有界循环缓冲区的实现。 什么东西使用原子变量?

另外,我不确定为什么这些经典方法是为需要大量动态内存管理的链表设计的。 在多线程程序中,所有内存管理例程都是序列化的。 我们不是通过将它们与动态数据结构结合使用来破坏无锁方法的好处吗?

我试图在英特尔64位架构上使用pthread库在C / C ++中编写代码。

谢谢Shirish

使用圆形缓冲器需要锁定,因为需要阻塞以防止头部越过尾部。 但是否则头部和尾部指针可以很容易地以原子方式更新。 或者在某些情况下,缓冲区可能非常大,以至于覆盖不是问题。 (在现实生活中,你会在自动交易系统中看到这一点,循环缓冲区的大小可以容纳X分钟的市场数据。如果你落后X分钟,你会比覆盖你的缓冲区更糟糕的问题)。

当我在C ++中实现MS队列时,我使用堆栈构建了一个无锁分配器,这很容易实现。 如果我有MSQueue,那么在编译时我知道sizeof(MSQueue :: node)。 然后我制作一堆所需大小的N个缓冲区。 N可以增长,即如果pop()返回null,很容易向堆请求更多的块,并将它们推送到堆栈中。 在可能阻塞的更多内存调用之外,这是一个无锁操作。

请注意,T不能有非平凡的dtor。 我研究过一个允许非平凡dtors的版本,实际上是有效的。 但我发现将T指向我想要的T,生产者释放所有权,消费者获得所有权更容易。 这当然要求T本身是使用lockfree方法分配的,但我在堆栈中使用的相同分配器也在这里工作。

在任何情况下,无锁编程的要点都不是数据结构本身更慢。 要点是这样的:

  1. lock free使我独立于调度程序。 基于锁的编程取决于调度程序,以确保锁的持有者正在运行,以便他们可以释放锁。 这就是导致“优先级倒置”的原因在Linux上有一些锁定属性可以确保这种情况发生
  2. 如果我独立于调度程序,操作系统管理时间片的时间要容易得多,而且我的上下文切换要少得多
  3. 使用lockfree方法编写正确的多线程程序更容易,因为我不必担心死锁,活锁,调度,同步等等。这对于共享内存实现尤其如此,其中进程可能会在共享内存中锁定时死亡,并且没有办法释放锁
  4. 无锁方法更容易扩展。 实际上,我已经使用网络上的消息传递实现了无锁方法。 像这样的分布式锁是一场噩梦

也就是说,在许多情况下,基于锁定的方法是优选的和/或需要的

  1. 更新昂贵或无法复制的东西时。 大多数无锁方法使用某种版本控制,即制作对象的副本,更新它,并检查共享版本是否仍然与复制时相同,然后使当前版本更新版本。 Els再次复制它,应用更新,然后再次检查。 继续这样做直到它工作。 当对象很小但是它们很大,或包含文件句柄等时,这很好,不推荐使用
  2. 大多数类型都无法以无锁方式访问,例如任何STL容器。 它们具有需要非原子访问的不变量,例如assert(vector.size()== vector.end() - vector.begin())。 因此,如果要更新/读取共享的向量,则必须将其锁定。

这是一个老问题,但没有人提供一个公认的解决方案。 所以我为其他可能正在搜索的人提供此信息。

本网站: http//www.1024cores.net

提供一些非常有用的lockfree / waitfree数据结构,并提供详尽的解释。

您正在寻求的是读/写问题的无锁解决方案。

请参阅: http//www.1024cores.net/home/lock-free-algorithms/reader-writer-problem

对于传统的单块循环缓冲区,我认为这不能通过原子操作安全地完成。 你需要在一次阅读中做这么多。 假设你有一个具有以下结构的结构:

uint8_t* buf;
unsigned int size; // Actual max. buffer size
unsigned int length; // Actual stored data length (suppose in write prohibited from being > size)
unsigned int offset; // Start of current stored data

在阅读时你需要做以下(这是我实现它的方式,你可以交换一些步骤,比如我之后讨论):

  1. 检查读取长度是否超过存储长度
  2. 检查偏移量+读取长度是否超过缓冲区边界
  3. 读出数据
  4. 增加偏移量,减少长度

你应该做什么同步(如此原子)才能使这项工作? 实际上在一个原子步骤中组合步骤1和4,或澄清:执行此操作同步:

  1. 检查read_length,这可能像read_length=min(read_length,length);
  2. 使用read_length减小长度: length-=read_length
  3. 从offset unsigned int local_offset = offset获取本地副本
  4. 使用read_length增加偏移量: offset+=read_length

之后你可以从local_offset开始做一个memcpy(或其他),检查你的读取是否超过循环缓冲区大小(分成2个memcpy),.... 这是“非常”线程安全的,你的写入方法仍然可以写入你正在读取的内存,因此请确保你的缓冲区足够大,以尽量减少这种可能性。

现在,虽然我可以想象你可以将3和4结合起来(我猜这是他们在链表实例中所做的),甚至是原子操作中的1和2,我看不到你在一个原子操作中完成这个整个交易:)。

但是,如果您的消费者非常聪明并且总是知道要阅读什么,您可以尝试放弃“长度”检查。 你还需要一个新的woffset变量,因为用于确定写入偏移量的(offset + length)%size的旧方法将不再起作用。 请注意,这与链表的情况很接近,实际上您总是从列表中读取一个元素(=固定的,已知大小)。 此外,如果您将其设为循环链接列表,您可以阅读更多内容或写入您当时正在阅读的位置!

最后:我的建议,只是使用锁,我使用CircularBuffer类,对读取和写入完全安全)对于实时720p60视频流,我没有锁定速度问题。

这是一个古老的问题,但没有人提供准确答案的答案。 鉴于(几乎)相同问题的搜索结果仍然很高,应该有答案,因为存在一个答案。

可能有多个解决方案,但这里有一个实现: https//github.com/tudinfse/FFQ自述文件中引用的会议文件详述了算法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM