在典型的应用程序堆栈中,多个线程用于服务事件、处理数据、流水线等。一个重要的设计考虑是线程如何意识到有工作要做,一些通用方法包括:
线程请求休眠时的实际行为不仅因平台而异,而且因同一平台的不同版本和使用模式而异。
例如,POSIX 要求睡眠调用始终让出 CPU,而 Linux 允许睡眠实现(包括睡眠、usleep、nanosleep 等)在某些情况下忙于等待。对于具有固定计时器滴答声(通常为 100Hz、250Hz 或 1000Hz)的旧版本 Linux,屈服于调度程序时会有相对较大的惩罚,这鼓励在短时间内在睡眠调用中使用内部忙等待。相比之下,较新版本的 Linux 具有使用动态滴答的更复杂的调度程序,可以与休眠线程进行更准确的短期交互,这在很大程度上消除了忙等待以实现低休眠期的需要。
以下经验法则通常适用于标准进程的最新 Linux 版本(即那些在标准调度程序下以正常权限运行的进程):
虽然上面表明即使相对较短的 ~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 策略显示了响应时间的阶跃变化和稳定增长,反映了线程等待接收事件的时间越长,增量回退。