英特尔线程构建块并发队列:在pop_if_present()上使用pop()

时间:2010-02-18 00:00:06

标签: c++ concurrency multithreading

使用阻止调用pop()

相比有什么不同
while(pop_if_present(...)) 

哪个优先于另一个?为什么?

我正在寻求更深入地了解轮询自己之间的权衡,例如while(pop_if_present(...))关于让系统为你做这件事。这是一个非常普遍的主题。例如,使用boost::asio我可以执行阻止或执行以下操作的myIO.run()

while(1) 
{
myIO.poll()
}

一种可能的解释是,调用while(pop_if_present(...))的线程将保持忙碌,因此这很糟糕。但有人或某事必须轮询异步事件。当它被委托给操作系统或库时,为什么以及如何更便宜?是因为关于轮询的操作系统或智能库是否会进行指数退避?

2 个答案:

答案 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-lockcompare-and-swap等特殊处理器指令上的同步原语,通常在操作系统内部用于在中断处理程序和其他部分之间进行通信。内核,并构建更高级别的结构,如load-linked/store conditional

使用阻止pop(),如果队列为空,则进入睡眠等待,即要求操作系统将使用者线程置于不可调度的状态直到一个事件 - 推入队列 - 从另一个线程发生。关键在于处理器可用于其他(希望是有用的)工作。 TBB实现实际上很难避免睡眠,因为它很昂贵(进入内核,重新安排等)。目标是优化队列不为空的正常情况,并且可以快速检索项目。

选择非常简单 - 总是睡眠等待,即阻止pop(),除非你必须忙等待(并且是实时系统,OS中断上下文和一些非常专业的应用程序。)

希望这有点帮助。