线程池大小选择:针对 I/O 密集型场景和 CPU 密集型场景

I/O 密集型场景

I/O 密集型场景指的是系统的磁盘以及内存的性能要高于CPU 性能,因此系统在这种场景下,大部分时间消耗在磁盘/内存的读写,CPU 的利用率不高。比如一些包含网络传输的场景。通常我们不会希望 CPU 成为瓶颈,因为 CPU 毕竟是计算机中很昂贵的部分,我们希望 CPU 可以做更多的工作,因为我们会通过调整线程池大小来转移业务瓶颈,这将在下文提到。

CPU密集型场景

CPU 密集型则相反,指的是 CPU 的性能高于磁盘和内存,这时 CPU 的利用率为 100%,I/O 只需要很短的时间就可以完成。比如当使用 CPU 时(而不是 GPU),神经网络的前向和反向计算就是典型的 CPU 密集型场景,因为这其中涉及了大量的卷积和数值运算。

线程池大小选择

线程池在 I/O 密集型场景的必要性

在这两种场景下,线程池大小的选择也是不同的。在 I/O 密集型场景中,我们需要增大线程池的大小以通过 overlap 消除 CPU 等待 I/O 的时间。这样讲很抽象,可以参考下图,首先我们假设这是一个任务,红色部分表示 I/O 时间,白色部分表示 CPU 处理时间

单个任务


当线程池大小为1,也就是串行执行时,可以看到中间会有很多等待 I/O 的时间,CPU 被白白浪费:

单线程


当线程池大小为2,两个线程并发执行,这时候可以遮盖(overlap)I/O 的部分,从外部看 CPU 一直在执行,没有干等的时候:

多线程


可以看到合理设置线程池大小,可以将 I/O 的时间给遮盖掉,更好地利用 CPU 的资源。具体公式将在后续讲解。

线程池在 CPU 密集型场景的必要性

实际上,只有多核 CPU 的主机才适合使用线程池处理 CPU 密集型场景。我们举个例子,在单核 CPU 中,CPU 占用率为 100%,这时我们的线程池大小设置为2,那么在线程切换时需要有线程上下文切换的开销,这是需要时间的,而且线程的切换并没有意义,单个 CPU 的的利用率一直是 100%,相当于白白多了上下文切换的时间。而假如是 6 核的 CPU,这时可以将线程池大小设置为 6,这样 6 个 CPU 中都有自己的线程,处理速度可以提升 6 倍。

线程池大小计算

铺垫了前面的理论,我们这里具体给出线程池大小的计算公式。首先明确几个变量:

  1. Ncpu = CPU 的数量
  2. Ucpu = CPU 的目标使用率
  3. W / C = 等待 I/O 的时间和 CPU 计算时间的比
    为保持 CPU 的使用率,最优的线程池大小为:
    Nthreads = Ncpu x Ucpu x (1 + W / C)
    这个公式的由来我们可以参考《Linux 多线程服务器端编程》中的一个思路:
    如果线程池在执行任务时,CPU 计算所占的时间比重为 P,主机一共有 C 个 CPU,为了让 C 个 CPU 都跑到 100%,那么我们需要 C / P 个线程,所以也就是:
    Nthreads =(( I/O 时间 + CPU 计算时间 )/ CPU 计算时间) CPU 数量*
    也就是
    Nthreads = (( I/O 时间 / CPU 计算时间 )+ 1) CPU 数量*
    再加上目标的 CPU 利用率,就是上面提到的公式了。

所以我们可以分析在不同场景下的线程池大小计算,I/O 密集型场景,根据公式计算即可。CPU 密集型场景,根据公式应该是 CPU 核数,但是实际应用场景中通常用 CPU 核数 + 1,这个 + 1 实际上是为了保证当某个 CPU 上的线程因为缺页中断或某些原因暂停时,刚好有一个额外的线程来保证 CPU 不会空等,总之还是让 CPU 一直处于忙碌的状态。

总结

总的来说,线程 I/O 时间比例越高,需要越多线程来 overlap 这个 I/O,反之需要越少的线程。
同时需要认识到,并不是使用线程池就一定比单线程高效,单线程相比多线程避免了上下文切换时间和锁。因此也可以很高效。
有了以上方法,我们就可以做到在不同业务场景中选择不同的线程池大小了。

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...