6月2日 01:37

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. 多进程(最常用)

python
from 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:

c
Py_BEGIN_ALLOW_THREADS // 这里做纯 C 计算,不操作 Python 对象 result = heavy_computation(data); Py_END_ALLOW_THREADS

NumPy、Pandas、hashlib 等库在底层 C 代码中释放了 GIL,所以它们在多线程中可以并行运行。

3. 用 asyncio 替代多线程

IO 密集型任务不需要多线程,用协程就够了:

python
import 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:

  1. IO 操作:网络请求、文件读写、time.sleep() 等
  2. C 扩展主动释放:NumPy 运算、hashlib 哈希等

纯 Python 代码(循环、计算、字符串操作)永远不会释放 GIL。

追问

Python 3.13 的 no-GIL 模式能用吗?

实验阶段,不推荐生产使用。性能比有 GIL 模式慢约 10-15%,很多 C 扩展还不兼容。等 Python 3.14/3.15 稳定后再考虑。

多线程在 Python 里完全没用吗?

不是。IO 密集型场景(网络爬虫、API 调用、数据库查询)多线程比单线程快得多。只是 CPU 密集型场景多线程没有加速效果。

标签:Python