处理器核心数与线程池大小的比较

很多时候我听说最好将线程池中的线程数保持在该系统中的核心数之下。 拥有两次或更multithreading而不是内核数量不仅是一种浪费,而且还可能导致性能下降。

那些是真的吗? 如果没有,揭穿这些主张(特别是与Java有关)的基本原则是什么?

这些说法不是一般性声明 。 也就是说,有时候它们是真实的(或者是真实的),有时它们显然是错误的。

有几件事无疑是真的:

  1. 更multithreading意味着更多内存使用。 每个线程都需要一个线程堆栈。 对于最近的HotSpot JVM, 最小线程堆栈大小为64Kb,默认值可以高达1Mb。 这可能很重要。 此外,任何活动的线程都可能拥有或共享堆中的对象,无论它是否当前是可运行的。 因此,期望更multithreading意味着更大的内存工作集是合理的。

  2. 与执行硬件上的内核(或超线程内核或其他内容)相比,JVM实际上不能运行更multithreading。 如果没有引擎,汽车将无法运行,如果没有核心,线程将无法运行。

除此之外,事情变得不那么明确了。 “问题”是活动线程可以处于各种“状态”。 例如:

  • 一个活动线程可以运行; 即积极执行指令。
  • 活动线程可以运行; 即等待核心以便它可以运行。
  • 活动线程可以通过同步; 即等待来自另一个线程的信号,或等待锁定被释放。
  • 活动线程可以等待外部事件; 例如,等待一些外部服务器/服务来响应请求。

“每个核心一个线程”启发式假定线程正在运行或可运行(根据上述内容)。 但是对于很多multithreading应用程序来说,启发式是错误的……因为它没有考虑其他状态中的线程。

现在“太多”线程显然导致显着的性能下降,使用太多内存很简单。 (想象一下,你拥有4Gb的物理内存,你可以用1Mb堆栈创建8,000个线程。这是虚拟内存抖动的一个秘诀。)

但其他事情呢? 有太multithreading会导致过多的上下文切换吗?

我不这么认为。 如果你有很multithreading,你的应用程序使用这些线程会导致过多的上下文切换,这对性能不利。 但是,我认为上下文切换的根本原因不是实际的线程数。 性能问题的根源更可能是应用程序:

  • 以特别浪费的方式同步; 例如,当Object.notify()更好时,使用Object.notifyAll() ,OR
  • 同步高度竞争的数据结构,或
  • 相对于每个线程正在执行的有用工作量进行太多同步,或者
  • 试图并行执行太多I / O.

(在最后一种情况下,瓶颈可能是I / O系统而不是上下文切换……除非I / O是IPC与同一台机器上的服务/程序。)

另一点是,在没有上述混淆因素的情况下,拥有更multithreading不会增加上下文切换。 如果你的应用程序有N个可运行的线程竞争M个处理器,并且线程纯粹是计算和无争用的,那么OS’线程调度程序将尝试在它们之间进行时间分片。 但是,时间片的长度很可能以十分之一秒(或更长)来衡量,因此与CPU绑定线程在其切片期间实际执行的工作相比,上下文切换开销可以忽略不计。 如果我们假设时间片的长度是常数,那么上下文切换开销也将是常量。 添加更多可运行的线程(增加N)不会显着改变工作与开销的比率。


总之,“太multithreading”确实对性能有害。 然而,实际上有多少“太多”并没有很好的通用“经验法则”。 并且(幸运的是)在性能问题变得显着之前,您通常会有相当大的余地。

线程数少于核心数通常意味着您无法利用所有可用内核。

通常的问题是你想要的核心数多于核心数。 但是,这取决于线程花费在I / O上的时间(总体)与计算所花费的时间之间的差异。 如果他们都在进行纯计算,那么您通常需要与核心数相同的线程数。 如果他们做了相当多的I / O,你通常需要比核心更多的线程。

从另一个方向看一下,你需要运行足够的线程来确保每当一个线程由于某种原因阻塞(通常在I / O上等待)时,你可以在该核心上运行另一个线程(没有阻塞)。 确切的数字取决于每个线程花费多少时间被阻止。

除非线程数远远超过核心数,否则这不是真的。 原因是额外的线程意味着额外的上下文切换。 但事实并非如此,因为如果这些上下文切换是有益的,操作系统将只进行非强制上下文切换,而其他线程不会强制进行额外的上下文切换。

如果创建了一个荒谬的线程数,那就浪费了资源。 但是,与创建太少线程的糟糕程度相比,这一切都没有。 如果创建的线程太少,则意外的阻塞(例如页面错误)可能导致CPU处于空闲状态,并且会从一些额外的上下文切换中淹没任何可能的伤害。

不完全正确,这取决于整体软件架构。 如果某些线程被OS暂停,因为它们正在等待I / O完成,那么保留比可用内核更多的线程是有原因的。 这可以是显式I / O调用(例如从文件同步读取),也可以是隐式的,例如系统分页处理。

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