Python GIL 是什么?为什么多线程不能利用多核?怎么绕过?
GIL(Global Interpreter Lock)是 CPython 的一把全局互斥锁,同一时刻只允许一个线程执行 Python 字节码。这意味着 Python 多线程无法利用多核 CPU 做计算密集型任务。但 IO 密集型任务不受影响——线程等待 IO 时会释放 GIL。
GIL 为什么存在
CPython 的内存管理(引用计数)不是线程安全的。如果多个线程同时修改 ob_refcnt,可能导致内存泄漏或提前释放。GIL 是最简单的解决方案——一个线程执行 Python 代码时,其他线程不能运行。
为什么不去掉?Python 核心开发者试过多次,去掉 GIL 会导致单线程性能下降 10-30%(因为要加细粒度锁替代全局锁),C 扩展也需要大量改写。社区不愿意接受这个代价。
Python 3.13 引入了实验性的 free-threaded 模式(PEP 703),允许禁用 GIL,但目前还是实验阶段,性能和兼容性都不成熟。
GIL 的影响范围
| 任务类型 | 多线程表现 | 原因 |
|---|---|---|
| CPU 密集 | 比单线程还慢 | 线程争抢 GIL,切换开销大 |
| IO 密集 | 有效加速 | 等 IO 时释放 GIL,其他线程可运行 |
| 混合型 | 部分有效 | 计算部分受 GIL 限制 |
CPU 密集比单线程还慢是因为 GIL 切换本身有开销,多线程反而增加了竞争。
绕过 GIL 的三种方式
1. 多进程(最常用)
pythonfrom multiprocessing import Pool def heavy_compute(n): return sum(i * i for i in range(n)) with Pool(4) as p: results = p.map(heavy_compute, [10**7] * 4) # 4 个进程并行
每个进程有独立的 GIL,真正并行。缺点:进程间通信成本高(数据要序列化),启动慢。
2. C 扩展释放 GIL
在 C 扩展中做计算密集型操作时,可以手动释放 GIL:
cPy_BEGIN_ALLOW_THREADS // 这里做纯 C 计算,不操作 Python 对象 result = heavy_computation(data); Py_END_ALLOW_THREADS
NumPy、Pandas、hashlib 等库在底层 C 代码中释放了 GIL,所以它们在多线程中可以并行运行。
3. 用 asyncio 替代多线程
IO 密集型任务不需要多线程,用协程就够了:
pythonimport asyncio import aiohttp async def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text() async def main(): tasks = [fetch(url) for url in urls] results = await asyncio.gather(*tasks)
单线程 + 事件循环,没有 GIL 问题,也没有线程切换开销。
GIL 的释放时机
CPython 在两种情况下释放 GIL:
- IO 操作:网络请求、文件读写、time.sleep() 等
- C 扩展主动释放:NumPy 运算、hashlib 哈希等
纯 Python 代码(循环、计算、字符串操作)永远不会释放 GIL。
追问
Python 3.13 的 no-GIL 模式能用吗?
实验阶段,不推荐生产使用。性能比有 GIL 模式慢约 10-15%,很多 C 扩展还不兼容。等 Python 3.14/3.15 稳定后再考虑。
多线程在 Python 里完全没用吗?
不是。IO 密集型场景(网络爬虫、API 调用、数据库查询)多线程比单线程快得多。只是 CPU 密集型场景多线程没有加速效果。