process - thread - cpu

01. 进程、线程与 CPU 调度

0. 本章先解决什么问题

一个 CPU 核心同一时刻通常只能执行一个执行流,但系统里可能有成百上千个任务。

操作系统要解决:

谁现在运行?
运行多久?
谁等待?
谁被唤醒?
切换时保存什么?
为什么线程多了反而变慢?

本章要讲清:

  • 程序、进程、线程的区别。
  • 进程拥有什么资源,线程共享什么资源。
  • CPU 调度器如何让多个任务并发推进。
  • 线程状态、阻塞、唤醒、上下文切换是什么。
  • 为什么线程数量、阻塞比例、上下文切换会影响性能。

调度状态与就绪队列

这张图怎么读

这张图强调一个常被误解的点:runnable 不等于正在运行。线程可能已经具备运行资格,但还在就绪队列里等 CPU;也可能因为 IO、锁、条件变量进入阻塞状态,事件到来后再回到就绪队列。

1. 程序、进程、线程

概念含义
程序存在磁盘上的代码和资源
进程程序运行后的资源容器
线程进程内的执行流,CPU 调度的基本单位之一

直觉:

程序 = 菜谱
进程 = 开起来的一家厨房,拥有空间、工具、权限
线程 = 厨师,真正执行步骤

一个程序可以启动多个进程。一个进程可以有多个线程。

2. 进程包含什么

一个进程通常包含:

  • 独立虚拟地址空间。
  • 代码段、数据段、堆、栈、映射区。
  • 打开的文件描述符。
  • 当前工作目录。
  • 环境变量。
  • 权限和用户信息。
  • 资源限制。
  • 一个或多个线程。

进程是资源隔离单位。

进程之间默认不能直接访问彼此内存。要通信,需要 OS 提供的 IPC、文件、管道、Socket、共享内存等方式。

3. 线程共享什么、私有什么

同一进程内线程共享:

共享资源说明
地址空间能访问同一堆和全局数据
打开的文件文件描述符属于进程资源
网络连接Socket 也通常是文件描述符
权限和环境进程级信息

每个线程私有:

私有资源说明
程序计数器当前执行到哪里
寄存器上下文被切换时要保存恢复
函数调用、局部变量、返回地址
线程局部状态运行时或系统维护的线程私有信息

共享让线程通信方便,也让数据竞争成为可能。

4. CPU 调度在做什么

CPU 核心数量有限,线程数量可能很多。

调度器决定:

  • 哪个线程运行。
  • 在哪个 CPU 核心运行。
  • 运行多长时间。
  • 优先级如何影响选择。
  • 阻塞线程什么时候被唤醒。
  • 是否迁移到其他核心。

简化流程:

线程 A 运行
-> 时间片用完 / 阻塞 / 更高优先级任务到来
-> 保存 A 的上下文
-> 选择线程 B
-> 恢复 B 的上下文
-> B 运行

这就是上下文切换。

5. 线程状态

不同系统和运行时状态命名不同,但可以先用通用模型:

状态直觉
new创建了,还没运行
runnable可以运行,正在等 CPU 或已经在 CPU 上
running正在 CPU 上执行
blocked等锁、IO、事件、资源
sleeping/timed waiting等时间到或事件到
terminated执行结束

重要点:

runnable 不等于正在运行。

它可能只是具备运行资格,仍在调度队列里等 CPU。

6. 阻塞和唤醒

线程可能因为很多原因阻塞:

阻塞原因例子
IO等文件、网络、设备
等别的线程释放锁
条件变量等队列非空
sleep等时间到
join/wait等另一个任务完成

阻塞时,OS 可以把 CPU 让给其他 runnable 线程。

事件发生后,线程被唤醒,重新进入可运行队列。

running -> blocked -> runnable -> running

唤醒不代表立刻运行。它还要等调度。

7. 上下文切换为什么有成本

上下文切换成本包括:

  • 保存旧线程寄存器。
  • 恢复新线程寄存器。
  • 进入调度器。
  • 用户态/内核态切换。
  • Cache 局部性变差。
  • TLB 或地址空间相关缓存受影响。
  • 调度队列管理。

所以线程不是越多越好。

线程过多时,系统可能花大量时间:

切换线程
唤醒线程
竞争锁
维护队列

有效计算反而减少。

机制深挖:排队延迟和运行时间不是一回事

一个请求变慢时,线程本身可能只运行了很短时间,但排队等 CPU、等锁、等 IO 花了很久。把时间拆开更清楚:

时间段线程状态常见原因
等待进入线程池还没有线程执行它任务队列堆积、线程池满
runnable 等 CPU可运行但没上 CPUCPU 饱和、优先级低、quota 限制
running 执行正在 CPU 上真实计算、解析、压缩、加密
blocked 等资源不占 CPUIO、锁、条件变量、外部服务
被唤醒后再排队runnable唤醒不等于立刻运行

所以排障报告里最好不要只写“执行耗时 2s”。更有价值的是:

排队多久
真正运行多久
阻塞多久
被唤醒后又等了多久

这也是为什么 CPU 不高时请求仍可能慢:线程大部分时间可能在 blocked;CPU 很高时吞吐仍低:线程可能在频繁切换或争锁,真正有效计算少。

8. CPU 密集和 IO 密集

线程数量要看任务类型。

任务类型特点线程策略
CPU 密集大部分时间在计算接近 CPU 核数更合理
IO 密集大部分时间在等待 IO可以更多线程或异步模型
混合型计算和等待都有根据阻塞比例调节

如果 CPU 密集任务开太多线程,会增加上下文切换。

如果 IO 密集任务只有很少线程,CPU 可能在等待时闲置。

9. 抢占式调度和协作式调度

抢占式调度:

OS 可以在时间片到达或高优先级任务到来时切走当前线程。

协作式调度:

任务主动让出执行权。

现代通用 OS 通常使用抢占式调度,保证一个任务不主动让出也不能长期霸占 CPU。

用户态运行时或协程系统可能使用协作式或混合调度。

关键是:

最终能在 CPU 上运行的,仍然要落到 OS 可调度的执行实体。

10. 多核和负载均衡

多核系统可以真正并行运行多个线程。

调度器还要考虑:

  • 哪个核心空闲。
  • 线程是否适合留在原核心。
  • Cache 亲和性。
  • NUMA 等更复杂拓扑。
  • 负载是否均衡。

线程迁移到另一个核心可能让原本热的 Cache 数据失效,带来性能损耗。

所以调度不是简单“哪里空就扔哪里”。

11. 联系实际:线程排障怎么看

遇到程序卡住或慢,先问:

  1. 线程是在 running、runnable,还是 blocked?
  2. 如果 blocked,在等 IO、锁、条件、sleep,还是外部资源?
  3. runnable 很多但 CPU 不够吗?
  4. 上下文切换是否很高?
  5. 线程数是否远超需要?
  6. 是否有大量短任务创建销毁线程?
  7. 是否线程池耗尽,任务排队?
  8. 是否某个 event loop 被阻塞?

典型映射:

现象可能原因
CPU 高但吞吐低忙等、锁竞争、切换过多
CPU 不高但请求慢IO 阻塞、队列等待、线程池满
大量线程 blocked锁竞争或资源等待
大量 runnableCPU 不够或任务太多
延迟尖刺调度延迟、上下文切换、缺页、锁

手推:线程数翻倍,为什么吞吐可能不升反降

假设一台机器同时只有 4 个 CPU 核能跑用户任务。现在有 4 个纯计算线程,每个线程都一直 runnable:

4 cores
4 runnable threads
=> 每个线程大体能持续占用一个核心

如果把线程数改成 40:

4 cores
40 runnable threads
=> 任意时刻仍然只有 4 个线程在运行
=> 其余 36 个线程在 runnable 队列里等

吞吐不一定提高,因为真正的计算资源没有增加;额外线程还会带来:

成本为什么
上下文切换调度器要频繁保存/恢复执行现场
Cache 变冷线程切来切去,刚热起来的数据被换掉
锁竞争更多线程同时争同一份共享状态
内存压力每个线程都有栈和运行时元数据
尾延迟升高任务在 runnable 队列里等更久

所以线程数调优不是“越多越并发”,而是要先问:

瓶颈是 CPU 计算,还是 IO 等待?
线程是在 running,还是 runnable 排队?
增加线程是在增加有效工作,还是增加调度和争用?

对 CPU 密集任务,线程数远超可用核心通常会把问题从“计算”变成“调度”。对 IO 密集任务,更多线程可能有用,但前提是这些线程大部分时间确实在等待外部事件,而不是一起争 CPU 或锁。

边界条件:blocked 少不代表系统健康,runnable 多才是危险信号之一

初学排障时容易只盯 blocked:

blocked 多 -> 卡住
blocked 少 -> 没事

这不够。一个系统也可能没有大量 blocked,却仍然很慢:

大量线程都是 runnable
CPU 已经满
每个线程都只能分到很短时间片
请求在 runnable 队列里等待

这类问题的指纹是:

现象含义
runnable 数远大于核心数等 CPU 的任务太多
CPU 使用率高但业务吞吐低有效计算少,争用/切换/忙等多
上下文切换频繁线程过多或同步设计导致频繁让出
延迟长尾明显一部分任务排队等到很晚才真正运行

因此线程排障至少要把线程分成三类:

  • running:正在 CPU 上执行
  • runnable:想运行,但还没拿到 CPU
  • blocked/waiting:暂时不能运行,正在等某个条件

只有这三类分清楚,才能判断下一步是减线程、拆锁、限流、改异步,还是查外部 IO。

排障卡:卡住时先分类线程状态

线程排障不要先猜代码哪行错。先数状态:

状态分布可能方向下一步
running 少、blocked 多IO、锁、条件、外部资源等待查等待对象和持有者
runnable 很多、CPU 满CPU 不够或任务过多查热点、线程数、队列
runnable 很多、CPU 不满调度受限、资源限制、运行时阻塞查 quota、优先级、线程池
上下文切换很高线程太多、锁竞争、短任务过多降低线程数、批量、减少争用
event loop running 但全局慢单个执行流被慢 handler 占住查阻塞调用和长计算

最小记录应该写成:

多少线程 runnable
多少线程 blocked
blocked 在等什么
谁持有资源
上下文切换是否异常
队列是否在增长

这比一句“程序卡住了”更接近可执行诊断。

12. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 程序、进程、线程有什么区别?
  2. 进程为什么是资源容器?
  3. 线程共享什么,私有什么?
  4. 调度器为什么要保存和恢复上下文?
  5. runnable 为什么不等于正在运行?
  6. 阻塞和唤醒的流程是什么?
  7. 为什么线程不是越多越好?
  8. CPU 密集和 IO 密集任务应该如何理解线程数量?
  9. 程序卡住时,如何从线程状态和调度角度排查?

进程和线程不是抽象名词,它们是 OS 管理 CPU 时间、资源隔离和并发执行的核心工具。

延伸阅读