假设我们有一个单生产者线程单消费者线程无锁队列,并且生产者可能会长时间不产生任何数据。当队列中没有任何东西时,让消费者线程休眠是有益的(为了节省电力并为其他进程/线程释放CPU)。如果队列不是无锁的,解决这个问题的直接方法是让生产线程锁定互斥锁,执行其工作,发出条件变量信号并解锁,并为读取线程锁定互斥锁,等待条件变量,做它的阅读,然后解锁。但是,如果我们使用的是无锁队列,那么使用互斥锁的方式完全相同,就会消除我们首先使用无锁队列所获得的性能。
天真的解决方案是让每个插入队列后的生产者锁定互斥锁,发出条件变量信号,然后解锁互斥锁,保持实际工作(插入队列)完全锁定,并拥有消费者做同样的事情,锁定互斥锁,等待条件变量,解锁它,将所有内容从队列中拉出,然后重复,保持锁定之外的队列读取。这里有一个竞争条件:在读取器拉出队列并进入睡眠状态之间,生产者可能已经将一个项目插入队列中。现在读者将进入睡眠状态,并且可以无限期地保持这种状态,直到生产者插入另一个项目并再次发出状态变量信号。这意味着您偶尔会遇到特定的项目,这些项目似乎需要很长时间才能通过队列。如果您的队列始终处于活动状态,这可能不是问题,但如果它始终处于活动状态,您可能完全忘记了条件变量。
AFAICT解决方案是让生产者的行为与使用常规需求锁定队列的行为相同。它应该锁定互斥锁,插入无锁队列,发出条件变量信号,解锁。但是,消费者的行为应该不同。当它唤醒时,它应立即解锁互斥锁,而不是等到它读取队列。然后它应尽可能多地拉出队列并处理它。最后,只有当消费者想要进入睡眠状态时,它是否应该锁定互斥锁,检查是否有任何数据,然后如果解锁并处理它,或者如果没有,则等待条件变量。这样,互斥锁的争用频率低于锁定队列的频率,但是没有数据仍然留在队列中的睡眠风险。
这是最好的方法吗?还有其他选择吗?
注意:通过'最快',我的意思是'最快没有专注核心来反复检查队列',但这不适合标题; p
一个替代方案:使用天真的解决方案,但让消费者等待条件变量,其超时对应于您愿意为队列中的项目所容忍的最大延迟。如果所需的超时时间相当短,则可能低于操作系统的最短等待时间,或者仍占用过多的CPU。
答案 0 :(得分:1)
我假设你正在使用来自Dr Dobbs article的无锁单生产者单一消费者队列 - 或类似的东西 - 所以我将使用那里的术语。
在这种情况下,您在“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
这确保了在消费者趋于忙碌的情况下,当您知道消费者仍然醒着(并且不打算睡觉)时,您可以避免锁定互斥锁。它还可以最大限度地减少互斥锁持有的时间,从而进一步降低争用的可能性。
上面的解释非常冗长(因为我想要包含其工作原理的解释,而不仅仅是算法的解释),但由此产生的代码应该非常简单。
当然,它实际上是否值得做,取决于很多事情,我鼓励你衡量性能是否对你至关重要。在大多数情况下,mutex / condvar原语的良好实现在内部使用原子操作,因此它们通常只进行内核调用(最昂贵的位!)如果需要 - 即是否需要阻塞,或者肯定有一些线程在等待 - 所以不调用互斥函数节省的时间可能只会影响库调用的开销。