先给结论:GIL(全局解释器锁)是 CPython 的一把进程级互斥锁,它强制同一时刻只有一个线程能执行 Python 字节码。 这意味着哪怕你开了 8 个线程、跑在 8 核 CPU 上,纯 Python 的 CPU 密集型任务也只能在单核上“轮流跑”,根本享受不到多核并行。很多人写了多线程以为能加速,结果耗时反而更久了——这不是你的代码有问题,是 GIL 在背后“单线调度”。
这篇文章把 GIL 的来龙去脉、它到底卡在哪、以及什么场景该用多线程、什么场景必须用多进程,一次讲透。
一、GIL 到底是什么:一句话说清本质
GIL 全称 Global Interpreter Lock(全局解释器锁),它是 CPython 解释器里的一把大锁。任何一个线程想要执行 Python 字节码,都必须先抢到这把锁;抢到之后才能跑,跑一小段时间(Python 3.2 之后默认 5 毫秒的时间片)就被强制释放,让给其他线程。
所以多线程在 CPython 里并不是真正的并行(parallel),而是“并发交替执行”(concurrent)。在任意一瞬间,仍然只有一个线程在跑 Python 代码。
注意:GIL 是 CPython 的实现细节,不是 Python 语言的规定。 Jython、IronPython 没有 GIL;Python 3.13 开始官方提供了实验性的“无 GIL 构建”(free-threaded),但默认解释器依然带 GIL。
二、为什么 CPython 非要留着 GIL?三个历史原因
很多人觉得 GIL 是“历史包袱、早该去掉”,但当年加它是有理有据的:
- 内存管理线程安全:CPython 用引用计数管理对象生命周期。多个线程同时改一个对象的引用计数,如果不加锁就会数错,导致对象被提前释放或内存泄漏。GIL 用最简单的方式保住了引用计数的安全。
- 单核时代的历史背景:GIL 诞生于 1997 年前后,那时候主流机器都是单核,多线程并行本来就没收益,“一把锁换简单和稳定”是合理权衡。
- C 扩展兼容性:海量 C 扩展(NumPy、Pillow 等)都假设了“单线程执行 Python”的假设,去掉 GIL 会让整个生态重写,成本极高。社区也多次尝试去 GIL(最著名的是 2009 年的 “die GIL” 提案和后来的 Gilectomy 分支),都因为要么性能回退、要么破坏 C 扩展而未能合入主线。
关键认知:GIL 的存在是为了“简单、稳定、兼容”,不是设计失误。它的代价只在“多核并行 CPU 密集任务”时才暴露。
三、GIL 到底卡在哪:两类任务要分开看
GIL 只在一种情况下是“致命伤”:CPU 密集型任务(大量纯 Python 计算,比如算数、循环、数据处理)。这类任务线程之间疯狂抢锁,还伴随上下文切换开销,多线程不仅不加速,甚至更慢。
而 I/O 密集型任务(网络请求、文件读写、数据库查询)几乎不受影响:线程在等待 I/O 时会主动释放 GIL,让别的线程去跑。所以爬虫、Web 服务用多线程/asyncio 依然很快。
下面用三段真实代码验证,别只听结论。
四、代码实测①:CPU 密集任务,多线程 vs 单线程
我们用“计算第 n 个斐波那契数”这种纯计算来压测。先看多线程版本:
import threading
import time
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
def cpu_task():
# 每个线程算一次大斐波那契,模拟 CPU 密集
fib(800000)
threads = []
start = time.time()
for _ in range(4):
t = threading.Thread(target=cpu_task)
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"多线程耗时:{time.time() - start:.2f} 秒")
再写成单线程顺序跑 4 次:
import time
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
start = time.time()
for _ in range(4):
fib(800000)
print(f"单线程耗时:{time.time() - start:.2f} 秒")
跑起来你会发现:多线程版本和单线程版本耗时几乎一样,甚至略慢。 原因就是 4 个线程在抢同一把 GIL,谁也没能真正并行,反而多了锁竞争和切换开销。这就是 GIL 最典型的“翻车现场”。
五、代码实测②:多进程才是真并行
想真正用满多核,要用 multiprocessing(每个进程有自己独立的 GIL):
import multiprocessing
import time
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
if __name__ == "__main__":
start = time.time()
with multiprocessing.Pool(4) as pool:
pool.map(fib, [800000] * 4)
print(f"多进程耗时:{time.time() - start:.2f} 秒")
同样 4 份计算,多进程版本耗时会接近单线程的 1/4(4 核并行)。因为 4 个进程各持一把自己的 GIL,互不干扰,操作系统把它们调度到不同核心上真正同时跑。这才是 CPU 密集任务该用的方案。
六、代码实测③:I/O 密集任务,多线程依然很快
换成“等 I/O”的任务,情况完全反转:
import threading
import time
def io_task():
# 模拟网络/磁盘等待,期间会释放 GIL
time.sleep(1)
start = time.time()
for _ in range(4):
t = threading.Thread(target=io_task)
t.start()
# 主线程等待(实际可 join)
time.sleep(4.2)
print(f"多线程 I/O 耗时:{time.time() - start:.2f} 秒")
4 个线程各 sleep 1 秒,但总耗时约 1 秒而不是 4 秒——因为 sleep 会释放 GIL,线程们在等待时并不占用锁。所以爬虫、接口调用这类 I/O 密集场景,多线程/asyncio 依然是最佳选择,别被 GIL 吓退。
七、Python 3.2 之后 GIL 有过一次关键改进
早期 GIL 是“固定轮转”:不管线程在干嘛,跑 100 条字节码就换人,切换很频繁、开销大。Python 3.2(2011 年)引入了新的 GIL 实现:
- 改成基于时间片(默认 5ms)+ 优先级的调度;
- 正在执行的线程如果没用完时间片、又没遇到 I/O 释放,会优先继续持有锁,减少无谓切换;
- I/O 线程释放 GIL 后会通过条件变量立刻唤醒等待者,降低延迟。
这次改进让“多线程 vs 单线程”的差距不再那么离谱,但并没有改变“单核执行”的本质——CPU 密集任务依然无法并行加速。
八、不同任务该选哪种并发模型?一张表说清
| 任务类型 | 多线程 | 多进程 | asyncio | 说明 |
|---|---|---|---|---|
| CPU 密集(计算/循环) | ❌ 不加速 | ✅ 真并行 | ❌ 单线程 | 用 multiprocessing / ProcessPoolExecutor |
| I/O 密集(网络/文件) | ✅ 快 | ⚠️ 重 | ✅ 最快 | 多线程或 asyncio 都行 |
| 混合(算一点等一点) | ⚠️ 部分 | ✅ 稳 | ✅ 佳 | 进程池跑计算 + 异步跑 I/O |
| 调用释放 GIL 的 C 扩展 | ✅ 能并行 | ✅ 能并行 | ✅ 能并行 | NumPy/Numba 计算时释放 GIL |
怎么选:看到“大量纯 Python 循环/计算”就用多进程;看到“等网络/磁盘”就用多线程或 asyncio;如果计算靠 NumPy、Numba 这类 C 扩展,多线程也能并行,因为它们会在计算时主动释放 GIL。
九、想彻底绕开 GIL?4 个现实方案
- CPU 密集 →
multiprocessing/concurrent.futures.ProcessPoolExecutor:最常用、最稳,把任务拆到多个进程。日常推荐用进程池,写法比裸multiprocessing更简洁:
from concurrent.futures import ProcessPoolExecutor
import time
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
if __name__ == "__main__":
start = time.time()
with ProcessPoolExecutor(max_workers=4) as ex:
results = list(ex.map(fib, [800000] * 4))
print(f"进程池耗时:{time.time() - start:.2f} 秒,结果数:{len(results)}")
ProcessPoolExecutor 帮你管好了进程创建、任务分发和结果回收,CPU 密集任务直接 ex.map 投进去就行,比自己写 multiprocessing.Pool 更少样板代码。
- 用释放 GIL 的库:NumPy、Pandas(底层 C)、Numba、Pillow 做数值/图像处理时,内部会释放 GIL,多线程也能加速。
- 异步 + 多进程混合:asyncio 管 I/O,进程池管计算,各取所长。
- 等 free-threaded Python:Python 3.13 起实验性支持
--disable-gil的“无 GIL 构建”(PEP 703),未来可能成为默认;但目前生态兼容性还不够,生产环境暂不建议。
提醒:不要为了“绕过 GIL”盲目上多进程。多进程有进程间通信、内存占用的成本;I/O 场景老老实实用多线程/asyncio 反而更简单高效。
十、小结:记住这 5 句话就够了
- GIL 是 CPython 的进程级互斥锁,强制单时刻只有一个线程执行字节码,所以多线程是“并发”不是“并行”。
- 它只卡 CPU 密集任务,I/O 密集任务因为会释放 GIL,多线程依然很快。
- 想用满多核做计算,用
multiprocessing,不是threading。 - NumPy/Numba 这类 C 扩展计算时会释放 GIL,多线程也能并行。
- GIL 是历史权衡不是 bug,Python 3.13 的 free-threaded 是未来方向,但当下多进程最稳。
你平时写多线程是为了加速计算、还是为了并发处理请求?评论区聊聊你踩过的 GIL 坑,我看看大家最常中招的是哪类场景。