GIL(Global Interpreter Lock)是 CPython 的全局互斥锁:同一时刻一个进程内只有一个线程能执行 Python 字节码。它决定了「多线程不能并行计算」,也解释了 Python 并发的全部选型逻辑。
是什么,为什么存在
GIL 保护的是解释器自身的状态,核心是引用计数:每个对象带 ob_refcnt,增删引用时 +1 / -1,归零即回收。「读-改-写」三步不是原子的,无锁并发操作计数会丢失——对象要么提前被释放(use-after-free),要么永久泄漏。给每次计数操作加细粒度锁代价过高(死锁风险、维护成本、单线程也变慢),CPython 选择了最粗粒度的方案:一把全局大锁。
第二层是历史包袱:GIL 让 C 扩展不必自行处理线程安全即可工作,NumPy 等生态在其上长成;移除 GIL 要求大量扩展改写,阻力长期无法克服。
影响:CPU 密集 vs I/O 密集
CPU 密集任务多线程毫无加速,甚至因切换与争抢更慢:
import threading
def count(n=10**7):
while n:
n -= 1
# 单线程跑两次 vs 两线程并发跑两次:耗时几乎相同
# 两线程还要付线程切换 + GIL 争抢的额外成本I/O 密集任务多线程仍然有效:线程进入阻塞系统调用(read / accept / sleep)前会主动释放 GIL;同时解释器强制持锁线程按切换间隔让出,其他线程得以利用阻塞空窗执行字节码。爬虫、文件 IO、数据库访问用线程仍能并发。
import sys
sys.setswitchinterval(0.005) # 切换间隔(秒),默认5ms;调大减少切换,调小提高响应C 扩展可在重计算前后手动释放 GIL(Py_BEGIN_ALLOW_THREADS 宏),NumPy 的矩阵运算、zlib 压缩即如此——这段时间多线程真正并行,「NumPy 重活开线程有效」正源于此。
绕过路径
| 场景 | 方案 |
|---|---|
| CPU 密集 | multiprocessing 多进程:多解释器各持一把 GIL,真正并行 |
| CPU 密集(热点已库化) | 交给 NumPy / PyTorch 等释放 GIL 的 C 扩展 |
| I/O 密集、高并发 | asyncio:单线程事件循环,无锁无争抢 |
| I/O 密集、少量并发 | threading 多线程:阻塞等待时释放 GIL |
多进程的 fork / spawn 启动语义见 Unix 系统调用。3.13+ 可查询当前解释器是否带 GIL:
import sysconfig
sysconfig.get_config_var("Py_GIL_DISABLED") # None/0=常规构建带GIL,1=free-threading构建演进:分解释器与去 GIL
- 3.12(PEP 684):per-interpreter GIL——每个子解释器独立一把 GIL,单进程内可真正并行;但要求扩展与模块状态完全隔离,普通代码短期用不上。
- 3.13(PEP 703):free-threading 实验性构建(
python3.13t)。用偏向引用计数(biased reference counting)、延迟回收等机制替代大锁保护;单线程约慢 10%,多线程 CPU 密集可真并行。3.14 起持续完善,成为默认仍需数个版本过渡。 - 短期现实:默认 CPython 仍有 GIL,并发设计按上表选型。
Note
GIL 只属于 CPython;PyPy、Jython 等实现没有或另有方案。日常说「Python 有 GIL」默认指 CPython。