深色模式
01 · GIL 与多线程:Python 为什么「多线程不快」
学完你能说出:
- GIL 是什么:同一时刻只允许一个线程执行 Python 字节码。
- 为什么 IO 密集型任务用多线程仍然有效,CPU 密集型却没用。
- CPU 密集型任务该用什么:多进程。
先看地图
这是「Python 与工程」模块的第一篇,接着进程与线程讲。操作系统篇讲的是通用原理,这一篇讲 Python 特有的一个限制。AI 应用岗面试问 Python,这题出现频率最高。
本篇讲 4 个点:① GIL 是什么 → ② 为什么有 GIL → ③ IO 密集 vs CPU 密集 → ④ 怎么选:线程、进程、协程。
一句话本质
GIL(全局解释器锁)= CPython 解释器里的一把大锁:同一时刻,只有拿到锁的那个线程能执行 Python 代码。
生活类比:只有一支麦克风的会议
- 会议室里有 4 个人(4 个线程),但只有一支麦克风(GIL)。谁拿着麦克风谁才能说话。
- 如果大家都要一直说话(CPU 密集:一直在算),4 个人轮流用麦克风,总的说话时间和 1 个人差不多,没有变快。
- 如果大家大部分时间在等别人回邮件(IO 密集:等网络、等磁盘),等的时候把麦克风放下,别人就能用。4 个人一起等,总时间大大缩短。
一步步看:两个实验(数字是示意)
实验 1:CPU 密集:计算 1 到 1 亿的平方和,做 4 次。
| 方式 | 耗时 |
|---|---|
| 单线程,顺序做 4 次 | 约 20 秒 |
| 4 个线程 | 约 20 秒(甚至更慢,因为切换线程有开销) |
| 4 个进程 | 约 5 秒(4 核 CPU 真正同时算) |
实验 2:IO 密集:请求 4 个网页,每个要等 1 秒。
| 方式 | 耗时 |
|---|---|
| 单线程,顺序请求 | 约 4 秒 |
| 4 个线程 | 约 1 秒 |
为什么实验 2 有效?线程在等网络时会主动释放 GIL,别的线程就能拿到锁去发下一个请求。等待是可以重叠的。
为什么这么设计
为什么 CPython 要有 GIL?
- 问题:CPython 用引用计数管理内存:每个对象记着「有几个变量在用我」,计数归零就释放。如果多个线程同时修改同一个计数,可能算错,导致对象被提前释放(程序崩溃)或永远不释放(内存泄漏)。
- 方案:加一把全局大锁,同一时刻只让一个线程执行,引用计数就不会被同时修改。
- 好处:解释器实现简单、单线程速度快,C 扩展库也更容易写对。代价是多线程无法利用多核来加速计算。
一句话答法
GIL 是为了保护 CPython 的引用计数等内部状态不被多线程同时修改,用一把全局锁换来实现简单和单线程性能,代价是 CPU 密集型多线程无法并行。
为什么 IO 密集用线程,CPU 密集用进程?
- 问题:GIL 让多线程无法同时计算。
- 方案:
- IO 密集:线程在等 IO 时会释放 GIL,多个等待可以重叠 → 用多线程(或协程)。
- CPU 密集:每个进程有自己独立的解释器和 GIL,可以真正同时跑在多个核上 → 用多进程。
- 好处:两类任务都能用上合适的并发方式。代价是进程更重:创建慢、不共享内存,数据要在进程之间传递。
一句话答法
IO 等待时会释放 GIL,所以 IO 密集用多线程有效;CPU 密集用多进程,每个进程有独立的 GIL,能真正利用多核。
怎么选:线程、进程、协程
| 场景 | 推荐 | 原因 |
|---|---|---|
| 并发调用很多 API(比如批量调大模型) | 协程(asyncio)或多线程 | IO 密集,等待可以重叠;协程开销更小,见第 04 篇 |
| 数据处理、数值计算 | 多进程,或用 NumPy 等库 | CPU 密集;NumPy 底层是 C 代码,计算时会释放 GIL |
| 简单脚本 | 单线程 | 别过早优化 |
英文术语拆解
| 术语 | 拆开看 | 一句话意思 |
|---|---|---|
| GIL | Global 全局 + Interpreter 解释器 + Lock 锁 | 全局解释器锁 |
| CPython | C 语言实现的 Python | 官方、最常用的 Python 解释器 |
| Reference Counting | reference 引用 + counting 计数 | 引用计数:记录对象被引用了几次 |
| CPU-bound | CPU + bound 受限于 | CPU 密集型:时间主要花在计算上 |
| IO-bound | IO(输入输出)+ bound | IO 密集型:时间主要花在等待网络、磁盘上 |
| threading | thread 线程 + -ing | Python 的多线程标准库 |
| multiprocessing | multi 多 + processing 处理(进程) | Python 的多进程标准库 |
面试怎么问
Q:Python 有 GIL,多线程还有用吗?
有用,适合 IO 密集型任务。线程在等待网络、磁盘时会释放 GIL,其他线程可以继续执行,所以多个 IO 等待能重叠。CPU 密集型任务因为 GIL 无法并行,要用多进程,或者用在计算时会释放 GIL 的 C 扩展(如 NumPy)。
Q:为什么会有 GIL?
CPython 用引用计数管理内存,GIL 保证同一时刻只有一个线程执行字节码,避免引用计数等内部状态的竞争,使解释器实现简单、单线程性能好。
Q:GIL 会被去掉吗?
Python 3.13 起提供了实验性的「自由线程」(无 GIL)构建版本,但默认版本仍然有 GIL,生态还在适配中。实际开发中仍按「IO 用线程或协程、CPU 用进程」来选。
容易混的地方
- 有 GIL ≠ 不需要锁:GIL 只保证单条字节码执行时不被打断,
count += 1这种「读 → 加 → 写」由多条字节码组成,多线程下照样会出错,共享数据仍然要加锁。 - GIL 是 CPython 的特性,不是 Python 语言的规定:其他实现(如 Jython)没有 GIL。
- 多进程不是万能的:进程间传数据要序列化,数据很大时开销可能超过并行带来的收益。
和其他知识的连接
- ← 来自 进程与线程:进程隔离、线程共享内存,所以 GIL 只锁同一进程内的线程。
- ← 来自 调度与并发同步:GIL 本质是一把互斥锁。
- → 影响 async/await 异步编程:IO 密集的另一种解法。
自测 3 问
Q1:用「一支麦克风」类比,解释 GIL 对 CPU 密集和 IO 密集任务的不同影响。
点开看答案
GIL 像唯一的一支麦克风。CPU 密集时大家都要一直说话,只能轮流,总时间不变;IO 密集时大家大多在等回复,等的时候把麦克风放下,别人可以用,等待时间可以重叠,所以变快。
Q2:CPython 为什么要有 GIL?
点开看答案
为了保护引用计数等解释器内部状态不被多线程同时修改(否则对象可能被提前释放或泄漏),用一把全局锁换取实现简单和单线程性能。
Q3:要并发调用 100 次大模型 API,用什么?为什么?
点开看答案
用协程(asyncio)或多线程。这是 IO 密集任务,时间主要花在等网络响应上,等待时会释放 GIL,请求可以重叠进行。协程开销更小,适合大量并发。
✅ 验收清单
- 能用一句话说清 GIL 是什么
- 能用「麦克风」类比解释为什么 IO 密集用线程有效、CPU 密集无效
- 能讲清 GIL 存在的原因(问题 → 方案 → 好处 / 代价)
- 能根据任务类型,选择线程、进程或协程,并说出理由
- 能解释「有 GIL 为什么还需要加锁」
下一步该懂什么
- 处理大文件、大批数据时,怎么不一次性占满内存?→ 迭代器、生成器与 yield