什么是无锁队列轮询的最快方法?

6
假设我们有一个单生产者线程、单消费者线程、无锁队列,并且生产者可能长时间不生产数据。在队列中没有任何数据时,让消费者线程休眠会很有益(节省电力并释放CPU以供其他进程/线程使用)。如果队列不是无锁的,则解决此问题的直接方法是让生产线程锁定互斥锁,执行其工作,发出条件变量信号并解锁,然后让读取线程锁定互斥锁,等待条件变量,进行读取,然后解锁。但是,如果我们使用无锁队列,使用互斥锁的方式将消除我们从一开始使用无锁队列获得的性能。
天真的解决方案是,在每次插入队列之后,使生产者锁定互斥锁,发出条件变量信号,然后解锁互斥锁,将实际工作(插入队列)完全保留在锁外,并使消费者执行相同的操作,锁定互斥锁,等待条件变量,解锁它,将所有东西从队列中取出,然后重复,将队列的读取保持在锁外。这里存在竞态条件:在读者从队列中取出并进入睡眠状态之间,生产者可能已经向队列中插入了一个项目。现在,读者将进入睡眠状态,并可能一直保持这种状态,直到生产者再次插入另一个项目并再次发出条件变量信号。这意味着您偶尔可能会发现特定项目需要很长时间才能通过队列传输。如果您的队列始终处于活动状态,则可能不会有问题,但是如果它始终处于活动状态,则可以完全忘记条件变量。
据我所知,解决方案是使生产者的行为与使用常规需锁定队列时相同。它应该锁定互斥锁,插入无锁队列,发出条件变量信号,然后解锁。但是,消费者应该有所不同。当它醒来时,它应立即解锁互斥锁,而不是等待读取队列。然后,它应尽可能多地拉取队列并处理它。最后,只有当消费者想要进入睡眠状态时,它才应锁定互斥锁,检查是否有任何数据,然后如果有,则解锁并处理它,否则等待条件变量。这样,互斥锁的争用次数比锁满队列少,但没有风险在队列上留下数据就进入睡眠状态。
这是最好的方法吗?还有其他选择吗?
注意:'最快'实际上是指'最快而不需专用核心反复检查队列',但这无法适合标题;p 一个替代方案:采用天真的解决方案,但让消费者等待条件变量,等待时间与您愿意容忍项目通过队列所需的最大延迟相对应。但是,如果所需的超时时间相当短,则可能低于您的操作系统的最小等待时间或仍会消耗太多的CPU。

生产者每次生产东西时不能发出条件变量的信号吗?为什么需要互斥锁? - Gabe
@Gabe:有一个可能会让你认为的误解是,如果没有任何东西在等待条件变量时发出信号,那么下一次有东西等待条件变量时它就会立即被唤醒,但实际情况并非如此。如果当信号被触发时你不在等待条件变量,那么就好像它从未发生过一样。从这个意义上说,等待条件变量与在文件/套接字/管道上使用poll/select是不同的。 - Joseph Garvin
抱歉,我以为你的问题更通用。你应该编辑问题,提到你正在使用pthread。 - Gabe
@Gabe:它的意图是通用的,我只是不能检查每个操作系统的线程API 。但我相当确信几乎所有的API都需要使用互斥锁与条件变量。这也适用于boost的线程API和win32的线程。我认为从条件变量中唤醒时保持锁定是它们定义的一部分,这意味着必须涉及互斥锁。 - Joseph Garvin
我记得Windows 7允许用户级线程,你可以在程序中控制它们 - 比如暂停。你可以很容易地编写一个无锁互斥量,问题不在于互斥量,而在于挂起线程的问题。操作系统通常不提供此API。现在有了这个API,你可以编写一个轻量级的无锁互斥量,并在需要空闲时挂起自己的线程。 - user82238
显示剩余2条评论
1个回答

1

我假设您正在使用来自Dr Dobbs文章或类似物的无锁单生产者单消费者队列,因此我将使用其中的术语。

在这种情况下,您在以“ AFAICT”开头的段落中提出的答案很好,但我认为它可以稍微优化:

在消费者方面 - 正如您所说,当消费者耗尽队列并考虑休眠(仅在这种情况下),它会锁定互斥量,再次检查队列,然后要么释放互斥量并继续工作(如果队列中有新项),要么在条件变量上阻塞(自然地在醒来时释放互斥量以发现非空队列)。
在生产者方面: 首先复制last,称其为saved_last 像往常一样添加new_item,然后复制divider指针,称其为saved_divider。 如果saved_divider的值等于new_item,也就是你刚刚插入的对象,则你的对象已经被消耗,你的工作完成了。 否则,如果saved_divider的值不等于saved_last,则你不需要唤醒消费者。这是因为: 在你添加新对象之后的某个时间,你知道divider还没有到达new_item或saved_last 自从你开始插入以来,last只有这两个值 消费者只有在divider等于last时才停止 因此,消费者必须仍然醒着,并将在休眠之前到达你的新项目。 否则锁定互斥量,信号condvar然后释放互斥量。(在消费者注意到队列为空并实际上在condvar上阻塞之间的时间内,获得互斥锁确保您不会发出condar信号。)

这样做可以确保在消费者繁忙时,你避免在消费者仍然醒着(而不是即将入睡)的情况下锁定互斥量。它还最小化了持有互斥量的时间,以进一步减少争用的可能性。

上述解释比较啰嗦(因为我想包括为什么它有效的解释,而不仅仅是算法),但由此产生的代码应该很简单。

当然,是否值得这样做取决于很多因素,如果性能至关重要,我建议你进行测量。互斥/条件变量原语的良好实现在大多数情况下内部使用原子操作,因此它们通常只在需要时进行内核调用(最昂贵的部分!)- 即如果需要阻塞或肯定有一些线程正在等待,则通过不调用互斥函数节省的时间可能仅相当于库调用的开销。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接