🐧 第四章 进程调度器
📄 一句话概括

八个互不相干的活儿,核数 1→2→4→8 时总耗时 80→40→20→10,完美线性。但只要活儿之间要互相等,加核就远不到线性——加核能不能提速,取决于瓶颈在哪。

📝 本节和前面几节的关系

4.11 讲的是单核上的分配,这一节把核数变成变量:核多了以后,调度多了两个新问题——谁上哪个核、负载怎么平衡。

一、多核带来的两个新问题

一、亲和性(谁上哪个核)。一个进程在某个核上跑热了,换到别的核缓存全凉。所以调度器尽量让它待在原核。 二、负载均衡。有的核上排了一堆,有的核闲着一半。调度器要定期把任务搬过去——而搬一次也是一次切换加一次缓存失效。 这两件事在单核时代根本不存在,所以多核调度不是【单核调度乘几份】。

二、完美线性的条件

八个一样的活儿、每个 10 拍、互相不干扰:一核 80 拍、八核 10 拍。这是完美线性——8 倍核换来 8 倍速。 条件有三个:活儿能拆开、彼此不共享东西、没有别的瓶颈。三个条件缺一个,加速比就掉下来。 判据很实用:你只有在【三个条件全满足】时才该期待线性加速。任何共享资源(锁、同一块内存、同一个磁盘)都会把你拉回来。

三、IO 密集时加核没用

八个活儿、每跑 5 拍就等 20 拍 IO:加核确实有加速,但远不到线性——八核只快了 3 倍出头,不是 8 倍。 因为一部分时间卡在 IO 上,CPU 再多也只能干等。核是给【算】用的,不是给【等】用的。 这个结论在工程上很值钱:一台 IO 密集的服务加 CPU 核,钱花了、QPS 涨不了多少。该换的是存储或缓存。

四、现实中多核调度的样子

每个核有自己的就绪队列,不是所有核抢一个队列——抢一个队列会带来严重的锁竞争。 核之间定期做负载均衡:把任务从忙的核搬到闲的核。 NUMA 机器上更复杂:跨节点访问内存比本地慢几倍,所以调度器还要考虑【进程和它的内存是不是在同一个节点】。 所以你看到的【负载不均匀】,很多时候不是 bug,是调度器在【少搬以减少缓存失效】。

五、动手 Demo:多个逻辑 CPU——核多了,活就能快点干完吗

两组实验:八个互不相干的活儿(应该线性),与八个每跑 5 拍就等 20 拍 IO 的活儿(应该远不到线性)。 重点看第二张表的加速比。

# ① 八个独立活儿
for ncpu in (1, 2, 4, 8):
    ps = [Proc("P%d" % i, 10) for i in range(1, 9)]
    done, sw, _tl, total = run(ps, quantum=4, ncpu=ncpu)

# ② 八个爱等的活儿
for ncpu in (1, 2, 4, 8):
    ps = [Proc("P%d" % i, 10, io_each=5, io_len=20) for i in range(1, 9)]
    done, sw, _tl, total = run(ps, quantum=2, ncpu=ncpu)

实跑输出:

--------------------------------------------------------------------
多个逻辑 CPU——核多了,活就能快点干完吗
--------------------------------------------------------------------
① 八个一样的活儿,核数从 1 加到 8

  核数     跑完用     加速比      切换次数
  -----------------------------------------
  1        80         1.00 倍     23
  2        40         2.00 倍     22
  4        20         4.00 倍     20
  8        10         8.00 倍     0

  **八个活儿、每个 10 拍:一核 80 拍,八核 10 拍——这时候是完美线性。**
  **因为每个活儿之间一点关系都没有,谁也不用等谁。**

② 但如果活儿之间要互相等

  核数     跑完用     比一核快多少
  ---------------------------------
  1        92         1.00 倍
  2        56         1.64 倍
  4        38         2.42 倍
  8        29         3.17 倍

  **这批活儿每跑 5 拍就要等 20 拍 IO——加核有用,但远不到线性。**
  **因为一部分时间卡在 IO 上,CPU 再多也只能干等。**
  **所以【加核能不能加速】取决于瓶颈在哪**:CPU 密集能线性,IO 密集基本不能。

第一张表是完美的:80/40/20/10,加速比 1/2/4/8。因为八个活儿之间一点关系都没有。 第二张表就掉下来了:八核只快了 3 倍出头。因为有一大半时间在等 IO,那段时间加核也没人用得上。 这就是【阿姆达尔定律】的直觉版:能并行的部分才受益,不能并行的部分(包括等待)原封不动。 推论:判断该不该加核,先看【CPU 占用率】。占用率上不去,加核是白花钱。

30 秒实验

  1. nproc 看你有几个逻辑核,再 lscpu | head 看物理核与超线程。
  2. taskset -c 0 ./程序 与不加 taskset 各跑一次,看耗时差多少。
  3. mpstat -P ALL 1 3(sysstat)看每个核的占用,多数时候是不均匀的。

常见误区

  1. 【核数翻倍,速度就翻倍】。只有【活儿能拆、不共享、没别的瓶颈】时才成立。三个条件缺一个就掉。
  2. 【多核一定能提高吞吐】。如果瓶颈在 IO 或锁,加核只会让它们抢得更凶。
  3. 【负载不均匀是 bug】。很多时候是调度器故意少搬,为了保住缓存局部性。
自测三问(点开对答案) 1. 多核给调度带来哪两个新问题? —— **亲和性**(让进程待在跑热的核上)与**负载均衡**(把任务从忙核搬到闲核)。 2. 完美线性加速的三个条件是什么? —— 活儿能拆开、彼此不共享资源、没有别的瓶颈。**缺一个,加速比就掉下来。** 3. IO 密集的负载加核为什么没用? —— 时间卡在等待上,**核是给【算】用的,不是给【等】用的**。该换的是存储或加缓存。

相关笔记