处理器核心数量与线程池大小的关系

23

我听说在线程池中保持的线程数应该低于系统的CPU核心数量,多于核心数的线程不仅会浪费资源,还可能导致性能下降。

这些说法是否正确?如果不正确,有哪些基本原则可以驳斥这些说法(特别涉及Java)?


1
相关:https://dev59.com/U2Yr5IYBdhLWcg3wQH-- - assylias
如果您有一个CPU绑定的进程,当您使用双倍于实际需要的CPU数量时,通常性能仅会下降不到5%。如果您有一个高延迟的IO绑定进程,则可能更多的线程会更好。如果资源瓶颈在本地资源上(例如硬盘),则您可能会发现,使用2个线程并不能比一个线程更快。 - Peter Lawrey
5个回答

30
许多时候我听说在线程池中保持的线程数量应该低于系统内核心的数量。拥有两倍或更多线程不仅是浪费,而且可能会导致性能下降。
这些说法“作为一般陈述”并不正确。也就是说,它们有时是正确的(或基本正确的),但其他时候则明显错误。
有几件事情是无可辩驳的:
1. 更多的线程意味着更多的内存使用。每个线程都需要一个线程堆栈。对于最近的HotSpot JVMs,最小线程堆栈大小为64Kb,而默认值可以高达1Mb。这可能非常重要。此外,任何存活的线程都很可能拥有或共享堆中的对象,无论它是否当前可运行。因此,合理地预期更多的线程意味着更大的内存工作集。
2. JVM 不能实际上运行比执行硬件上的核心(或超线程核心等)更多的线程。没有引擎汽车将无法运行,线程没有核心将无法运行。
除此之外,事情变得不那么明确了。问题在于,一个存活的线程可能处于各种“状态”。例如:
- 存活的线程可以正在运行;即积极地执行指令。 - 存活的线程可以是可运行的;即等待核心以便它可以运行。 - 存活的线程可以正在同步;即等待来自另一个线程的信号或等待锁被释放。 - 存活的线程可以等待外部事件;例如,等待某个外部服务器/服务响应请求。
“每个核心一个线程”的启发式假设线程要么正在运行,要么是可运行的(根据上述)。但对于许多多线程应用程序,这种启发式方法是错误的……因为它没有考虑到处于其他状态的线程。现在,“过多”的线程显然会导致严重的性能下降,简单来说就是使用了过多的内存(想象一下,如果您有4GB的物理内存并创建了8000个具有1MB堆栈的线程,那么这将导致虚拟内存抖动)。
但其他情况呢?过多的线程是否会导致过多的上下文切换?
我不这么认为。如果您有很多线程,并且应用程序对这些线程的使用可能导致过多的上下文切换,那么这对性能是有害的。然而,我认为上下文切换的根本原因并不是实际线程数。性能问题的根源更可能是应用程序:
- 以特别浪费的方式同步;例如,使用Object.notifyAll()时使用Object.notify()将更好,或者 - 在高度竞争的数据结构上同步,或者 - 相对于每个线程执行的有用工作量,进行了过多的同步,或者 - 尝试并行执行过多的I/O。
(对于最后一种情况,瓶颈可能是I/O系统而不是上下文切换...除非I/O是与同一台机器上的服务/程序之间的IPC。)
另一个要点是,在没有上述混淆因素的情况下,拥有更多的线程不会增加上下文切换。如果您的应用程序具有N个可运行线程竞争M个处理器,并且线程是纯计算并且无争用的,则操作系统的线程调度程序将尝试在它们之间进行时间片分配。但时间片的长度很可能以十分之一秒(或更长)为单位测量,因此与CPU绑定的线程在其片段期间实际执行的工作相比,上下文切换开销是可以忽略不计的。并且,如果我们假设时间片的长度是恒定的,那么上下文切换开销也将是恒定的。增加可运行线程数(增加N)不会显着改变工作与开销之比。总的来说,"过多的线程"对性能是有害的。然而,没有可靠的通用的"经验法则"可以规定多少是"过多"。幸运的是,在性能问题变得显著之前,通常您有相当大的余地。

8
拥有比处理器核心数量更少的线程通常意味着您无法利用所有可用的核心。通常的问题是您需要比核心更多的线程,然而这取决于线程总体上花费在I/O等任务和计算上的时间。如果它们只进行纯计算,则通常需要与核心数量相同的线程数。如果它们进行了相当数量的I/O,则通常需要比核心数量多得多的线程。
另一方面,您需要足够的线程运行以确保每当一个线程由于某种原因(通常是等待I/O)被阻塞时,您有另一个未被阻塞的线程可用于在该核心上运行。确切的线程数量取决于每个线程花费多少时间被阻塞。

如果它们正在进行大量的I/O操作,假设I/O由单独的处理器处理并使CPU处于空闲状态。 - kosa
@Nambari:当一个线程执行大量I/O操作时,该线程会被挂起,直到I/O操作完成。然后它变成了可运行状态,并且必须再次被调度。如果没有其他可运行的线程,则该核心在等待磁盘或其他操作完成时会被浪费。 - David Schwartz

5

这是不正确的,除非线程数远大于核心数。这样做的原因是额外的线程将意味着额外的上下文切换。但这并不是真的,因为操作系统只有在这些上下文切换有益时才会进行非强制性的上下文切换,并且额外的线程不会强制引发更多的上下文切换。

如果创建过多的线程,那么会浪费资源。但是,与创建过少的线程相比,这一点微不足道。如果创建的线程太少,意外的阻塞(如页面故障)可能导致CPU闲置,这比一些额外的上下文切换造成的任何可能的损害都要严重。


3

这并不完全正确,这取决于整体软件架构。保留比可用核心更多的线程是有原因的,以防一些线程被操作系统挂起,因为它们正在等待I/O完成。这可能是显式的I/O调用(例如从文件同步读取),也可能是隐式的,例如系统分页处理。

实际上,我曾在一本书中读到,将线程数保持为 CPU 核心数的两倍是一个好的做法。


1
其实我在一本书上读到,保持线程数是CPU核心数的两倍是一个好的做法。- 那是什么书? - undefined

2

对于REST API调用或者说I/O绑定操作,拥有比核心数更多的线程可以通过允许多个API请求并行处理来潜在地提高性能。然而,最佳线程数取决于各种因素,例如API请求频率、请求处理的复杂度以及服务器上可用的资源。

如果API请求处理是CPU绑定的,并且需要大量计算,则拥有太多线程可能会导致资源争用并降低性能。在这种情况下,线程数应限制为可用的核心数。

另一方面,如果API请求处理是I/O绑定的,并涉及等待来自外部资源(如数据库)的响应,则拥有更多线程可以通过允许多个请求并行处理来提高性能。

无论如何,建议进行性能测试以确定特定用例的最佳线程数,并使用响应时间、资源利用率和错误率等指标监控系统性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接