为说明问题,首先我们假设一个前提:负载流量与计算能力是相当的。理由是:如果负载流量小于计算能力,说明性能还有富余,还可以进一步加大负载流量。而如果负载流量大于计算能力,随着时间推移,必然有一部分计算工作以失败告终。
所以,我们在假定负载流量与计算能力相当的前提下,考察不同的线程调度策略会导致何种性能表现。
假设系统有 10 个 CPU,总的计算处理能力是“每秒钟处理 100 个用户请求”(一个 CPU 处理掉一个请求需要花费 100 毫秒),而负载流量也是“平均每秒钟发生 100 个用户请求”。
考虑两种调度策略:
a) 每个请求一旦到达,立刻启动一个线程对其进行处理,直至处理完毕返回;
b) 维持一个容量为 10 的线程池,当线程池为空时,新到来的请求将进入等待状态。
考虑两种极端的负载流量发生情况:
1) 每隔 1 秒钟有 100 个请求同时到达;
2) 每隔 10 毫秒有一个请求到达。
不难推测,稳态下:
a-1: 每个请求历时 1 秒钟完成(100 个线程被 10 个 CPU 均匀地推进,直至全部同时完成);
a-2: 每个请求历时 100 毫秒完成。
b-1: 有 10 个请求用时 100 毫秒完成,10 个请求用时 200 毫秒完成(其中前 100 毫秒处于等待状态),……。平均 500 毫秒。
b-2: 每个请求历时 100 毫秒完成。
这样看来,b 策略显然优于 a 策略。其核心指导思想是:当 CPU 已经都忙起来了,已经没有空闲的情况下,对于新到来的请求,与其让它跑起来,一起去均摊运行成本,不如让它先等着,这样至少已经跑着的线程能够尽快完成。实际生产环境下的负载流量发生情况应该是介于 1 和 2 之间的,但上述的思想原则应该仍然适用。
再进一步考虑,实际应用系统在处理一个请求时,不会完全是“纯计算”的内容,必然会涉及一些 IO 等待,那么,只有 10 个线程的线程池就显得小了,它将导致有些 CPU 在有些时间段处于空闲状态,不能充分发挥计算能力。应该考虑适当放大线程池的容量。那么,这个容量放大到多少合适呢?我想,基本的原则仍然是:争取让所有 CPU 都跑满即可,不要更多,在 CPU 跑满之前增加线程数是有益的,之后再增加就是有害的。甚至可以考虑动用“动态监测 CPU 状态”的手段进行线程调度。
反思以前对“线程池”的理解,往往简单化地把它类比于“数据库连接池”,认为“线程”是一种“构建开销很大的对象”,所以要预先创建好备用。(不知道别人如何,反正我自己以前从没有深入思考过这个问题)。结合上面的分析,可以说,线程池的真正价值在于优化线程调度策略,改善系统的综合性能表现,同时也便于实施对于过量负载的选择性拒绝(试想一下,如果过多的线程一起运行、一起超时、一起失败,该是多么糟糕的局面),保障系统能够健壮、平稳地运行。