任务分类
我们一般用一个线程池做同一种类型的任务,而不是把各种类型的任务都丢进同一个线程池执行。
而任务可以分成2种类型:CPU 密集型、IO密集型。
公式
先来看看2个公式,这两个公式适用任何一种类型。
公式一
Nthreads = Ncpu x Ucpu x (1 + W/C)
其中:
Ncpu = CPU的核心数量
Ucpu = CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
注意: Intel 引入超线程技术后,使核心数与线程数形成1:2的关系。1个CPU核心2个逻辑核心。
这里的 Ncpu,指逻辑核心数。在 Java 里,可以用如下代码获得逻辑核心数:
Runtime.getRuntime().availableProcessors();
公式二
Nthreads = Ncpu /(1 - 阻塞系数)
对比
我们希望这两个公式其实是互通的。假设公式一种的 CPU 使用率为1,即CPU可以100%运转。令公式一等于公式二,得 Ncpu x (1 + W/C) = Ncpu /(1 - 阻塞系数),推导得:
阻塞系数 = W / (W + C),即 阻塞系数 = 阻塞时间 /(阻塞时间 + 计算时间)
可以发现,这2公式其实是从不同的角度描述同一个东西。
联想
我们重点看公式一,因为 W/C 比 W / (W + C),更简洁。并且仍然不考虑 CPU 的使用率,假设它100%运转,因为本来就是要充分利用 CPU 嘛。
Nthreads = Ncpu x (1 + W/C)
W/C 我们现在可以理解为阻塞时间 / 计算时间。
这里有2种情况值得讨论:
- W = 0,这种情况CPU几乎没有阻塞的时候,一直在计算。这其实就是CPU密集型。
- W > C,这种情况阻塞时间比CPU计算时间还长,一般出现这种情况的,都是因为有IO,用户空间总是要等待内核空间准备好数据;甚至是网络IO,还要等待数据在网络中的传输。这其实就是IO密集型。
CPU密集型
对于这种类型,通过上面的分析,其实已经有答案了:
Nthreads = Ncpu,即线程数等于逻辑核心数。
抛开公式理解,为什么是逻辑核心数:
逻辑核心数决定了同时最多有多少个线程工作。线程数大于逻辑核心数,由于时间片轮转机制,肯定会有上下文的切换,从而浪费了CPU的性能。
但还有一种说法:
一个有Ncpu个处理器的系统通常通过使用一个Ncpu + 1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作)。
但是这种做法导致的多一个线程,上下文切换是否值得,需要自己考量。个人不建议这一种。
IO密集型
对于这种类型,关键在于 W > C,W 取多少合适。
这个需要测试,从 W = C 开始,即 Nthreads = 2Ncpu。线程数不会是越多越好,因为每一个线程都要占用一定的资源。
如果不想测试,那么就用 Nthreads = 2Ncpu 就好了,一般也都是这么设置的。