在事件循环中使用暂停器

问题描述

在典型的应用程序堆栈中,多个线程用于服务事件、处理数据、流水线等。一个重要的设计考虑是线程如何意识到有工作要做,一些通用方法包括:

  • 信号/通知:在这种情况下,接收线程让步(即,被添加到等待队列)直到被另一个线程通知。这样做的好处是资源消耗低;然而,有至少 20-50 微秒(可能更多——见下文)的相对较高的延迟来重新安排线程以响应信号。
  • 忙等待:在这种情况下,接收线程不断旋转,检查是否有工作要做的迹象。当线程有工作要做时,这具有快速响应(低延迟)的好处;然而,它是以高 CPU 使用率为代价的,在无事可做时浪费周期。此外,持续的高 CPU 使用率反过来会导致明显更高的电力需求和相关的冷却负载。
  • Fixed Sleep:在这种情况下,当没有进一步的工作要做时,接收线程会休眠一段固定的时间,然后再次检查是否有更多的工作。这具有低资源使用率的优势,但这种策略的明显缺点是最坏情况下的延迟至少与睡眠周期一样大。

睡眠问题,以及如何睡得香

线程请求休眠时的实际行为不仅因平台而异,而且因同一平台的不同版本和使用模式而异。

例如,POSIX 要求睡眠调用始终让出 CPU,而 Linux 允许睡眠实现(包括睡眠、usleep、nanosleep 等)在某些情况下忙于等待。对于具有固定计时器滴答声(通常为 100Hz、250Hz 或 1000Hz)的旧版本 Linux,屈服于调度程序时会有相对较大的惩罚,这鼓励在短时间内在睡眠调用中使用内部忙等待。相比之下,较新版本的 Linux 具有使用动态滴答的更复杂的调度程序,可以与休眠线程进行更准确的短期交互,这在很大程度上消除了忙等待以实现低休眠期的需要。

以下经验法则通常适用于标准进程的最新 Linux 版本(即那些在标准调度程序下以正常权限运行的进程):

  • ~1us 的睡眠请求原则上可以合理准确地得到服务
  • 一般来说,即使是很短的睡眠时间也不会忙等待,尽管极短的睡眠时间几乎肯定会
  • 与忙等待 (100%) 相比,~1ms 和~1us 的睡眠请求将 CPU 使用率分别降低到~1% 和~10%

虽然上面表明即使相对较短的 ~1us 睡眠也可能在延迟和资源使用之间提供有用的折衷,但主要问题是调度——一旦睡眠进程完全上下文关闭内核,重新调度的开销可以是数量级高于预期的睡眠时间。

同样,对于系统将如何运行,没有单一的答案。关键是尽量偏向情况,避免线程从一个核心切换,以及使用线程亲和性(避免线程被移动到另一个核心)和CPU隔离(避免另一个进程/线程争用线程)在这种情况下非常有效。(其他选项包括以实时优先级运行;但是,我们希望尽可能将本文档的重点放在标准设置上。)

谨慎使用亲和力、隔离和较短的睡眠周期可以产生响应迅速、低抖动的环境,与繁忙等待相比,这种环境使用的 CPU 资源要少得多。

什么是暂停器?

Chronicle 的 Pausers——一种开源产品——通过使用智能退避策略在信号/通知、固定睡眠和忙等待的上述极端行为之间提供滑动规模的行为,该策略可以实现更细微的控制以更好地平衡低延迟和资源利用率。

一般的策略是忙等待一小段时间,然后在没有工作可做时逐渐退回到越来越长的暂停(消耗越来越少的 CPU)。根据任务的不同,可以使用不同的策略(暂停模式),使用暂停的规范方式是:

暂停模式

Chronicle Pausers 允许针对给定级别的响应性和延迟优化 CPU 负载。可以高精度地配置此权衡,而无需对您的应用程序代码进行重大更改。例如,如果您意识到特定线程需要更快的响应,则可以将其暂停器从退避暂停器更改为忙碌暂停器,反之亦然。 

值得注意的是,用于内部最低延迟的 Busy Pauser 使用忙等待,因此将消耗 100% 的一个内核。因此,重要的是要确保 Busy Pausers 不会争用相同的内核,并且在使用 Busy Pausers 控制这方面时应考虑 CPU 亲和力和隔离。可以在此处找到有关 CPU 隔离及其在事件循环中的优势的更多信息。

暂停模式的表现

下图绘制了等待事件的时间(x 轴)与选择暂停器的暂停/响应时间的关系。

Busy、TimedBusy、Yielding 和 Millis Pausers 显示平坦的响应时间,无论线程等待接收事件的时间长短,但由于不同的让出策略与 CPU 使用率,响应时间也不同。在许多情况下,TimedBusy 尤其在低延迟和 CPU 使用率之间提供了极好的折衷。

Sleepy 和 Balanced 策略显示了响应时间的阶跃变化和稳定增长,反映了线程等待接收事件的时间越长,增量回退。

猜你喜欢

转载自blog.csdn.net/wouderw/article/details/128059272