1049 words
5 minutes
什么是 GIL:Python 全局解释器锁与并发误区
GIL 几乎是每个 Python 面试都会问到的点,但大多数答案只停留在”一句话定义”。这篇文章把它讲透:GIL 是什么、为什么 CPython 必须有它、什么时候会被释放,以及一个几乎人人会踩的坑——“有 GIL 就不用加锁”。
一、什么是 GIL
GIL(Global Interpreter Lock,全局解释器锁)是 CPython(官方 Python 解释器)中的一个互斥锁,作用是:同一时刻,只允许一个线程执行 Python 字节码。
- 为什么需要它:CPython 的内存管理使用引用计数(每个对象记录自己被引用了多少次),而引用计数操作不是线程安全的。如果多个线程同时修改引用计数,会导致内存泄漏或对象被过早释放。GIL 保证同一时刻只有一个线程在跑字节码,从而让引用计数操作保持原子,省去了给每个对象加锁的开销。
- 直接后果:多线程无法真正并行执行 CPU 密集型任务(哪怕机器有 8 核,纯 Python 计算也只有一个核在干活)。
注意:GIL 是 CPython 的产物。Jython、IronPython 没有 GIL,PyPy 也有类似机制但实现不同。你这里 Python 3.12 + 官方解释器,就是有 GIL 的 CPython。
二、什么时候释放 GIL
GIL 会在以下场景被释放,让其他线程有机会运行:
- 执行 I/O 操作时(最常见、也是多线程对 I/O 密集任务有效的原因)
- 网络请求、文件读写、磁盘操作、
time.sleep()、等待锁等——线程阻塞等待 I/O 时会主动释放 GIL,让别的线程去跑。
- 网络请求、文件读写、磁盘操作、
- 时间片轮转强制切换
- Python 3.2 起引入了间隔切换机制,默认每 5ms(
sys.getswitchinterval()可查,sys.setswitchinterval()可改)强制让出一次 GIL,即使线程没做 I/O。
- Python 3.2 起引入了间隔切换机制,默认每 5ms(
- 调用会主动释放 GIL 的 C 扩展
- 像
numpy的矩阵运算、pandas的部分底层计算,内部 C 代码会显式调用Py_BEGIN_ALLOW_THREADS释放 GIL,此时可以真正多核并行。
- 像
关键理解:纯 Python 的 CPU 密集代码(如一个大 for 循环做计算)几乎全程持有 GIL,只靠时间片切换串行交替执行,所以多线程对 CPU 密集任务基本无加速;真正能利用多核的是多进程(multiprocessing),或释放了 GIL 的 C 扩展。
三、互斥锁(同步锁)和 GIL 的区别
| 对比维度 | GIL | 互斥锁(threading.Lock) |
|---|---|---|
| 层面 | 解释器内部锁,保护 CPython 自身状态(主要是引用计数) | 用户代码锁,保护你自定义的共享数据/临界区 |
| 谁管理 | 解释器自动加锁/释放,程序员无法控制 | 程序员手动 acquire()/release()(或用 with) |
| 锁定的对象 | ”Python 字节码执行权”这个全局资源 | 某段临界区代码 / 某个共享变量 |
| 粒度 | 大,影响所有线程 | 小,只影响竞争同一把锁的线程 |
| 能否关闭/绕过 | 不能(CPython 固定存在) | 可自由创建多把锁、按需使用 |
最容易踩的坑:有 GIL ≠ 不用加锁
很多人误以为”有 GIL 所以多线程写数据是安全的”,这是错的。GIL 只保证单个字节码指令的原子性,而一行 Python 代码往往对应多条字节码,中途可能发生线程切换。
import threading
counter = 0
def add(): global counter for _ in range(1000000): counter += 1 # 这行不是原子操作!对应多条字节码
threads = [threading.Thread(target=add) for _ in range(2)]for t in threads: t.start()for t in threads: t.join()
print(counter) # 期望 2000000,实际往往小于它(竞态条件)counter += 1 内部其实是:读 counter → 加 1 → 写回 counter 三步,中间可能被切走,导致两个线程读到了同一个旧值,结果少加。正确做法是用互斥锁保护临界区:
import threading
counter = 0lock = threading.Lock() # 用户层面的互斥锁,和 GIL 是两回事
def add(): global counter for _ in range(1000000): with lock: # with 会自动 acquire / release counter += 1 # 临界区:加锁保证这段不被并发打断
threads = [threading.Thread(target=add) for _ in range(2)]for t in threads: t.start()for t in threads: t.join()
print(counter) # 2000000,正确 什么是 GIL:Python 全局解释器锁与并发误区
/posts/2026-8-15/什么是-gil/ Some information may be outdated