GIL 全局解释器锁
问题
什么是 GIL?为什么 Python 有 GIL?如何绕过 GIL 的限制?
答案
GIL 是什么
GIL(Global Interpreter Lock) 是 CPython 解释器中的一把全局互斥锁。任何时刻,只有一个线程能执行 Python 字节码。
GIL 存在的原因
- 保护 CPython 的引用计数:CPython 使用引用计数做垃圾回收,多线程同时修改引用计数会导致数据竞争
- 简化 C 扩展开发:C 扩展不需要自己处理线程安全
- 历史原因:早期单核 CPU 时代的设计决策
GIL 的影响
CPU 密集型:多线程无法利用多核,甚至比单线程更慢(线程切换开销)
import threading
import time
def count(n):
while n > 0:
n -= 1
# 单线程
start = time.time()
count(100_000_000)
print(f"单线程: {time.time() - start:.2f}s") # ~3.5s
# 多线程(因为 GIL,不会更快)
start = time.time()
t1 = threading.Thread(target=count, args=(50_000_000,))
t2 = threading.Thread(target=count, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"多线程: {time.time() - start:.2f}s") # ~3.8s(甚至更慢)
IO 密集型:GIL 在等待 IO 时会释放,多线程有效
import threading
import requests
def fetch(url):
return requests.get(url)
urls = ["https://example.com"] * 10
# 多线程处理 IO 密集型任务有明显加速
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
GIL 释放时机
| 场景 | GIL 状态 |
|---|---|
| 执行 Python 字节码 | 持有(每 5ms 检查一次是否释放) |
| IO 操作(网络、文件) | 释放 |
| C 扩展调用(如 NumPy) | 可以主动释放 |
time.sleep() | 释放 |
绕过 GIL 的方案
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 多进程 | 每个进程有独立 GIL | CPU 密集型 |
| C 扩展 | C 代码中可以释放 GIL | 计算密集 |
| NumPy | 底层 C 实现,释放 GIL | 数值计算 |
| asyncio | 单线程 IO 多路复用 | IO 密集型 |
| 子解释器 | 独立 GIL(3.12+) | 实验性 |
| Free-threading | 无 GIL(3.13+ 实验) | 未来方案 |
# 方案 1:多进程(最常用)
from multiprocessing import Pool
def cpu_task(n):
return sum(i * i for i in range(n))
with Pool(4) as pool:
results = pool.map(cpu_task, [10_000_000] * 4)
Python 3.13 Free-threading
PEP 703 引入了实验性的无 GIL 模式:
# 编译时启用
./configure --disable-gil
python -X gil=0 script.py
实验性特性
Free-threading 在 3.13 中是实验性的,许多 C 扩展还不兼容。预计 3-5 年后成为默认模式。
常见面试问题
Q1: GIL 是 Python 的特性还是 CPython 的特性?
答案:
GIL 是 CPython 实现的特性,不是 Python 语言的特性。其他实现如 Jython(Java)、IronPython(.NET)没有 GIL。PyPy 目前也有 GIL,但在研究去除方案。
Q2: 有了 GIL,Python 的多线程还有用吗?
答案:
有用。GIL 只影响 CPU 密集型任务。对于 IO 密集型任务(网络请求、文件操作、数据库查询),GIL 在等待 IO 时会释放,多线程能显著提升性能。
Q3: 为什么不直接去掉 GIL?
答案:
- 向后兼容性:去掉 GIL 会导致大量 C 扩展不兼容
- 单线程性能:去掉 GIL 后,单线程性能可能下降(需要细粒度锁)
- 复杂性:引用计数的线程安全需要原子操作,增加开销
这也是 PEP 703 采用渐进策略的原因。
Q4: asyncio 和多线程的区别?
答案:
| 对比 | asyncio | 多线程 |
|---|---|---|
| 执行方式 | 单线程协作式 | 多线程抢占式 |
| 切换开销 | 极低(用户态切换) | 较高(内核态切换) |
| 并发量 | 可达数万 | 通常几百到几千 |
| 竞态条件 | 较少(协作式) | 需要锁保护 |
| 编码风格 | async/await | 传统同步代码 |
| 生态依赖 | 需要异步库 | 可用同步库 |