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

Posted

技术标签:

【中文标题】英特尔线程构建块并发队列:在 pop_if_present() 上使用 pop()【英文标题】:Intel Thread Building Blocks Concurrent Queue: Using pop() over pop_if_present() 【发布时间】:2010-02-18 00:00:06 【问题描述】:

与使用阻塞调用pop()相比有什么区别,

while(pop_if_present(...)) 

应该优先选择哪个?为什么?

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

while(1) 

myIO.poll()

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

【问题讨论】:

【参考方案1】:

英特尔的 TBB 库是开源的,所以我看了一下...

看起来pop_if_present() 本质上检查队列是否为空,如果是则立即返回。如果没有,它会尝试获取队列顶部的元素(这可能会失败,因为另一个线程可能已经出现并获取了它)。如果未命中,它会在再次检查之前执行“atomic_backoff”暂停。 atomic_backoff 将简单地旋转它被调用的前几次(每次将其旋转循环计数加倍),但在一定数量的暂停之后,它只会屈服于操作系统调度程序,而不是假设因为它一直在等待而旋转一会儿,它还不如做得好。

对于普通的pop() 函数,如果队列中没有任何内容,则将执行atomic_backoff 等待,直到队列中有内容。

请注意,至少有两件有趣的事情(无论如何对我来说):

pop() 函数执行旋转等待(直到某个点)以等待队列中出现的某些内容;除非它必须等待很短的时间,否则它不会屈服于操作系统。所以正如你所料,没有太多理由让自己打电话给pop_if_present(),除非你在调用pop_if_present()之间还有其他事情要做

pop() 确实屈服于操作系统时,它通过简单地放弃它的时间片来实现。它不会阻塞同步对象上的线程,当将项目放置在队列中时可以发出信号 - 它似乎进入睡眠/轮询周期以检查队列中是否有要弹出的内容。这让我有点吃惊。

对此分析持保留态度...我用于此分析的源代码可能有点旧(它实际上来自 concurrent_queue_v2.h 和 .cpp),因为最近的 concurrent_queue 有不同的 API - 没有 @ 987654330@或pop_if_present(),只是最新class concurrent_queue接口中的try_pop()功能。旧接口已移至(可能有所更改)concurrent_bounded_queue 类。当库被构建为使用 OS 同步对象而不是忙等待和轮询时,似乎可以配置较新的 concurrent_queues。

【讨论】:

【参考方案2】:

使用while(pop_if_present(...)),您正在对队列进行蛮力busy wait(也称为旋转)。当队列为空时,您会通过保持 CPU 忙碌来浪费周期,直到某个项目被运行在不同 CPU 上的另一个线程推入队列,或者操作系统决定将您的 CPU 分配给其他一些可能不相关的线程/进程。

如果你只有一个 CPU,你可以看到这有多糟糕 - 生产者线程将无法推送并因此停止消费者旋转,至少直到消费者的 time quanta 结束加上 context switch 的开销.显然是个错误。

如果操作系统选择(或强制执行)生产者线程在不同的 CPU 上运行,那么对于多个 CPU,这可能会更好。这是spin-lock 的基本思想——一个直接建立在特殊处理器指令上的同步原语,例如compare-and-swap 或load-linked/store conditional,通常在操作系统内部用于中断处理程序和内核其余部分之间的通信,并构建更高的级别构造,例如semaphores。

通过阻塞pop(),如果队列为空,您将进入睡眠等待,即要求操作系统将消费者线程置于不可调度状态,直到事件 - 推入队列 - 从另一个线程发生。这里的关键是处理器可用于其他(希望有用的)工作。 TBB 实现实际上是在努力避免休眠,因​​为它代价高昂(进入内核、重新调度等)。目标是优化队列不为空且可以快速检索项目的正常情况。

虽然选择非常简单 - 总是休眠等待,即阻塞pop(),除非你必须忙等待(这是真实的 -时间系统、操作系统中断上下文和一些非常专业的应用程序。)

希望这会有所帮助。

【讨论】:

以上是关于英特尔线程构建块并发队列:在 pop_if_present() 上使用 pop()的主要内容,如果未能解决你的问题,请参考以下文章

C++ 并行化库:OpenMP 与线程构建块 [关闭]

使用英特尔线程构建模块编译:错误操作数到?:

线程块网格和多处理器

具有永久任务/线程的 TPL 数据流块

java-并发-concurrent综述

使用 task_group 的英特尔线程构建模块性能不佳(新用户)