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 | 可运行但没上 CPU | CPU 饱和、优先级低、quota 限制 |
| running 执行 | 正在 CPU 上 | 真实计算、解析、压缩、加密 |
| blocked 等资源 | 不占 CPU | IO、锁、条件变量、外部服务 |
| 被唤醒后再排队 | 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. 联系实际:线程排障怎么看
遇到程序卡住或慢,先问:
- 线程是在 running、runnable,还是 blocked?
- 如果 blocked,在等 IO、锁、条件、sleep,还是外部资源?
- runnable 很多但 CPU 不够吗?
- 上下文切换是否很高?
- 线程数是否远超需要?
- 是否有大量短任务创建销毁线程?
- 是否线程池耗尽,任务排队?
- 是否某个 event loop 被阻塞?
典型映射:
| 现象 | 可能原因 |
|---|---|
| CPU 高但吞吐低 | 忙等、锁竞争、切换过多 |
| CPU 不高但请求慢 | IO 阻塞、队列等待、线程池满 |
| 大量线程 blocked | 锁竞争或资源等待 |
| 大量 runnable | CPU 不够或任务太多 |
| 延迟尖刺 | 调度延迟、上下文切换、缺页、锁 |
手推:线程数翻倍,为什么吞吐可能不升反降
假设一台机器同时只有 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. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 程序、进程、线程有什么区别?
- 进程为什么是资源容器?
- 线程共享什么,私有什么?
- 调度器为什么要保存和恢复上下文?
- runnable 为什么不等于正在运行?
- 阻塞和唤醒的流程是什么?
- 为什么线程不是越多越好?
- CPU 密集和 IO 密集任务应该如何理解线程数量?
- 程序卡住时,如何从线程状态和调度角度排查?
进程和线程不是抽象名词,它们是 OS 管理 CPU 时间、资源隔离和并发执行的核心工具。