线程休眠时的线程与内核

Posted

技术标签:

【中文标题】线程休眠时的线程与内核【英文标题】:Threads vs cores when threads are asleep 【发布时间】:2019-11-21 20:02:50 【问题描述】:

我希望确认我对线程和 CPU 内核的假设。

所有的线程都是一样的。不使用磁盘 I/O,线程不共享内存,每个线程只做 CPU 绑定工作。

    如果我有 10 个内核的 CPU,并且我生成 10 个线程,每个线程将有自己的内核并同时运行。 如果我使用具有 10 个内核的 CPU 启动 20 个线程,那么 20 个线程将在 10 个内核之间“任务切换”,为每个线程提供大约每个内核 50% 的 CPU 时间。 如果我有 20 个线程,但其中 10 个线程处于休眠状态,而 10 个处于活动状态,则 10 个活动线程将以 100% 的 CPU 时间在 10 个内核上运行。 休眠的线程只消耗内存,而不消耗 CPU 时间。当线程还在休眠时。例如,10,000 个全部处于休眠状态的线程与 1 个处于休眠状态的线程使用相同数量的 CPU。 一般来说,如果您有一系列线程在处理并行进程时经常休眠。您可以添加更多线程,然后再添加核心,直到达到所有核心 100% 时间都处于忙碌状态的状态。

我的假设是否不正确?如果是,为什么?

编辑

当我说线程处于睡眠状态时,我的意思是线程被阻塞了一段特定的时间。在 C++ 中,我会使用 sleep_for 至少在指定的 sleep_duration 内阻止当前线程的执行

【问题讨论】:

“睡着了”究竟是什么意思?他们在“sleep(...)”通话中吗?等待锁定或通知?等待 I/O?以上任何一种? @StephenC 在我的情况下,我使用的是en.cppreference.com/w/cpp/thread/sleep_for 至少在指定的 sleep_duration 内阻止当前线程的执行。 所有假设对我来说都是正确的。 进程和线程调度是操作系统的一部分,所以你描述的行为真的取决于操作系统。 您可能正在与其他用户/系统进程...以及操作系统本身竞争 CPU/内核。 【参考方案1】:

如果我们假设您谈论的是在现代操作系统中使用本机线程支持实现的线程,那么您的陈述或多或少是正确的。

有几个因素可能导致行为偏离“理想”。

    如果有其他用户空间进程,它们可能会与您的应用程序竞争资源(CPU、内存等)。这将减少(例如)您的应用程序可用的 CPU。请注意,这将包括负责运行桌面环境的用户空间进程等。

    操作系统内核会产生各种开销。发生这种情况的地方很多,包括:

    管理文件系统。 管理物理/虚拟内存系统。 处理网络流量。 调度进程和线程。

    这将减少您的应用程序可用的 CPU。

    线程调度程序通常不会进行完全公平的调度。因此,一个线程可能比另一个线程获得更大比例的 CPU。

    当应用程序的内存占用很大并且线程没有良好的内存局部性时,会与硬件进行一些复杂的交互。由于各种原因,内存密集型线程相互竞争,并可能相互减慢。这些交互都被计为“用户进程”时间,但它们导致线程能够做的实际工作更少。


所以:

1) 如果我有 10 个内核的 CPU,并且我生成 10 个线程,每个线程将有自己的内核并同时运行。

由于其他用户进程和操作系统开销,可能并非总是如此。

2) 如果我用一个有 10 个核心的 CPU 启动 20 个线程,那么 20 个线程将在 10 个核心之间“任务切换”,为每个线程提供大约每个核心 50% 的 CPU 时间。

大约。有开销(见上文)。还有一个问题是相同优先级的不同线程之间的时间切片是相当粗粒度的,不一定公平。

3) 如果我有 20 个线程,但其中 10 个线程处于休眠状态,并且 10 个处于活动状态,那么这 10 个活动线程将以 100% 的 CPU 时间在 10 个内核上运行。

大约:见上文。

4) 休眠的线程只消耗内存,而不消耗 CPU 时间。当线程还在休眠时。例如,全部处于休眠状态的 10,000 个线程与 1 个处于休眠状态的线程使用相同数量的 CPU。

还有操作系统消耗CPU来管理休眠线程的问题;例如让他们入睡,决定何时唤醒他们,重新安排时间。

另一个问题是线程使用的内存也可能是有代价的。例如,如果用于所有进程的内存总和(包括所有 10,000 个线程的堆栈)大于可用的物理 RAM,则可能存在分页。这也使用 CPU 资源。

5) 通常,如果您有一系列线程在处理并行进程时经常休眠。您可以添加更多线程,然后再添加核心,直到达到所有核心 100% 时间都处于忙碌状态的状态。

不一定。如果虚拟内存使用不正常(即您正在大量分页),则系统可能不得不在等待从分页设备读取和写入内存页面时空闲一些 CPU。简而言之,您需要考虑内存利用率,否则会影响 CPU 利用率。

这也没有考虑线程调度和线程之间的上下文切换。每次操作系统将核心从一个线程切换到另一个线程时,它必须:

    保存旧线程的寄存器。 刷新处理器的内存缓存 使 VM 映射寄存器等无效。这包括 @bazza 提到的 TLB。 加载新线程的寄存器。 由于必须执行更多的主内存读取,以及由于先前的缓存失效而导致 vm 页面转换,从而导致性能下降。

这些开销可能很大。根据https://unix.stackexchange.com/questions/506564/,这通常是每次上下文切换大约 1.2 微秒。这听起来可能不多,但如果您的应用程序正在快速切换线程,那么每秒可能会达到很多毫秒。

【讨论】:

【参考方案2】:

正如 cmets 中已经提到的,这取决于许多因素。但在一般意义上,您的假设是正确的。

睡觉

在过去糟糕的日子里,sleep() 可能已经被 C 库实现为一个循环,做无意义的工作(例如,将 1 乘以 1,直到所需的时间过去)。在这种情况下,CPU 仍将 100% 忙碌。如今,sleep() 实际上会导致线程在必要的时间内被取消调度。 MS-DOS 等平台以这种方式工作,但几十年来任何多任务操作系统都有适当的实现。

10,000 个休眠线程会占用更多的 CPU 时间,因为操作系统必须在每个时间片滴答(每 60ms 左右)进行调度判断。它需要检查的线程越多是否准备好运行,检查所花费的 CPU 时间就越多。

翻译后备缓冲区

添加多于内核的线程通常被认为是可以的。但是您可能会在使用 Translate Lookaside Buffers(或它们在其他 CPU 上的等价物)时遇到问题。这些是 CPU 的虚拟内存管理部分,它们本身就是有效的内容地址内存。这真的很难实现,所以从来没有那么多。因此,内存分配越多(如果您添加越来越多的线程,就会出现这种情况),这种资源被消耗的越多,操作系统可能必须开始换入和换出 TLB 的不同负载以便可访问的所有虚拟内存分配。如果这种情况开始发生,那么过程中的一切都会变得非常非常缓慢。如今,这可能不像 20 年前那样成为问题。

此外,C 库中的现代内存分配器(以及因此构建在其之上的所有其他东西,例如 Java、C# 等等)实际上会非常小心地管理虚拟内存请求,从而最大限度地减少它们实际需要的时间操作系统以获得更多虚拟内存。基本上,他们寻求从他们已经获得的池中提供请求的分配,而不是每个malloc() 导致调用操作系统。这需要 TLB 的压力。

【讨论】:

我认为第二段具有误导性,因为现代操作系统调度程序几乎总是以这样一种方式设计的,即调度程序不需要在每个时间片滴答声上检查所有(甚至任何)睡眠线程(准确地说因为,正如您所指出的,当有许多睡眠线程时,这样做会导致 CPU 效率低下)。相反,休眠线程完全保存在一个单独的数据结构中,并且只有在发生触发移动的事件时才会移入(或移出)该数据结构。 @JeremyFriesner,这在很大程度上取决于底层硬件拥有哪些硬件计时器资源。理想情况下,操作系统能够为每个睡眠线程设置一个计时器,当计时器到期(“事件”)时触发的 ISR 会导致线程重新调度。然而,由于计时器资源有限,操作系统还有更多工作要做,数量不可避免地取决于有多少睡眠线程。必须使用一些计时器来引发事件,然后处理这些事件以确定要重新唤醒哪些线程, 你不需要为每个休眠线程设置一个单独的计时器;您只需要一个计时器,用于计划成为下一个唤醒的线程。当该计时器关闭时,您将该线程移出睡眠线程列表,将该线程从睡眠线程的优先级队列中弹出,并重置计时器以在安排下一个唤醒线程时关闭被唤醒。所有这些步骤都是 O(1) 操作。 @JeremyFriesner,是的,但是该优先级队列的管理不是每个线程的零 CPU 时间。必须将调用 sleep 的线程插入该队列中的正确位置。队列中已有的线程越多,这项任务就越耗时。我当然同意,一旦所有线程都处于休眠状态,并且不再添加,从队列中弹出下一个线程很短并且基本上是确定性的!

以上是关于线程休眠时的线程与内核的主要内容,如果未能解决你的问题,请参考以下文章

线程使用多个线程休眠的并行异步执行?

java多线程--线程休眠

线程操作之线程休眠

初学线程休眠和礼让

Java多线程系列---“基础篇”07之 线程休眠

Thread sleep()休眠