使用阻止调用pop()
与
while(pop_if_present(...))
哪个优先于另一个?为什么?
我正在寻求更深入地了解轮询自己之间的权衡,例如while(pop_if_present(...))
关于让系统为你做这件事。这是一个非常普遍的主题。例如,使用boost::asio
我可以执行阻止或执行以下操作的myIO.run()
:
while(1)
{
myIO.poll()
}
一种可能的解释是,调用while(pop_if_present(...))
的线程将保持忙碌,因此这很糟糕。但有人或某事必须轮询异步事件。当它被委托给操作系统或库时,为什么以及如何更便宜?是因为关于轮询的操作系统或智能库是否会进行指数退避?
答案 0 :(得分:3)
英特尔的TBB库是开源的,所以我看一下......
看起来pop_if_present()
基本上检查队列是否为空,如果是,则立即返回。如果没有,它会尝试将元素放在队列的顶部(这可能会失败,因为另一个线程可能已经出现并接受它)。如果它未命中,则在再次检查之前执行“atomic_backoff
”暂停。 atomic_backoff
只会旋转前几次调用它(每次旋转计数加倍),但经过一定数量的暂停后,它只会屈服于OS调度程序,而不是假设因为它是等了一会儿,它也可以做得很好。
对于普通pop()
函数,如果队列中没有任何内容将执行atomic_backoff
等待,直到队列中有某些内容为止。
请注意,至少有两件有趣的事情(对我而言):
pop()
函数对队列中显示的内容执行旋转等待(最多一点);它不会屈服于操作系统,除非它不得不等待一小会儿。正如您所料,除非您在调用pop_if_present()
pop_if_present()
。
当pop()
屈服于操作系统时,它只需放弃它的时间片即可。它不会阻止同步对象上的线程,当一个项目放在队列上时,该线程可以发出信号 - 它似乎进入睡眠/轮询周期以检查队列中是否有弹出的内容。这让我感到很惊讶。
用这种分析进行分析...我用于此分析的源可能有点旧(实际上来自concurrent_queue_v2.h和.cpp)因为更新的concurrent_queue具有不同的API - 没有{ {1}}或pop()
,只是最新pop_if_present()
界面中的try_pop()
函数。旧接口已被移动(可能有所改变)到class concurrent_queue
类。当构建库以使用OS同步对象而不是繁忙的等待和轮询时,似乎可以配置较新的concurrent_queues。
答案 1 :(得分:2)
使用while(pop_if_present(...))
,您正在队列中执行暴力busy wait(也称为旋转)。当队列为空时,通过保持CPU忙碌来浪费周期,直到某个项被另一个在不同CPU上运行的线程推入队列,或者操作系统决定将CPU提供给其他一些可能不相关的线程/进程。
如果您只有一个CPU,您可以看到这可能会有什么不好 - 生产者线程无法推送,从而阻止消费者转动,直到至少消费者的time quanta结束加上{ {3}}。显然是一个错误。
如果操作系统选择(或强制执行)生产者线程在不同的CPU上运行,那么使用多个CPU可能会更好。这是context switch的基本思想 - 直接构建在spin-lock或compare-and-swap等特殊处理器指令上的同步原语,通常在操作系统内部用于在中断处理程序和其他部分之间进行通信。内核,并构建更高级别的结构,如load-linked/store conditional。
使用阻止pop()
,如果队列为空,则进入睡眠等待,即要求操作系统将使用者线程置于不可调度的状态直到一个事件 - 推入队列 - 从另一个线程发生。关键在于处理器可用于其他(希望是有用的)工作。 TBB实现实际上很难避免睡眠,因为它很昂贵(进入内核,重新安排等)。目标是优化队列不为空的正常情况,并且可以快速检索项目。
选择非常简单 - 总是睡眠等待,即阻止pop()
,除非你必须忙等待(并且是实时系统,OS中断上下文和一些非常专业的应用程序。)
希望这有点帮助。