Python 3.13 终于能去掉 GIL 了?自由线程实测:多线程到底快了多少
前言
写过 Python 多线程的人大概都经历过一种困惑:明明机器有 8 个核,开了 8 个线程跑 CPU 密集型任务,速度却和单线程差不多,甚至更慢。罪魁祸首大家都听过——GIL(全局解释器锁)。多年来,Python 社区给出的标准答案是“CPU 密集型请用 multiprocessing,I/O 密集型才用 threading”。
Python 3.13 改变了这件事的剧本。它通过 PEP 703 引入了一个实验性的“自由线程”(free-threading)构建模式,把 GIL 真正关掉。这听起来像是一个迟到了二十年的礼物,但去掉 GIL 到底能快多少、要付出什么代价、现在能不能上生产,这些问题不能靠想象回答。本文会从 GIL 的原理讲起,再用一段可运行的基准测试代码,把自由线程、传统 GIL、multiprocessing 三者放在一起实测对比,并给出该不该用、怎么用的选型结论。
背景或问题
GIL 是什么,为什么它卡住了多线程
GIL(Global Interpreter Lock)是 CPython 解释器内部的一把互斥锁,它保证同一时刻只有一个线程在执行 Python 字节码。也就是说,哪怕你在一台 16 核机器上开了 16 个线程,它们在执行纯 Python 代码时也只能排成一条队,一个跑完让出 GIL,下一个才能上。
这个设计不是历史遗留的愚蠢,而是一次工程权衡。CPython 用引用计数做内存管理,对象的引用计数在多线程下会被并发修改,如果没有一把全局锁保护,就需要给每个对象加细粒度锁或用原子操作,这在 20 世纪 90 年代的实现成本和性能上都不划算。GIL 让 CPython 的内存管理变简单,也让写 C 扩展的作者几乎不用操心线程安全——因为反正只有一个线程在跑 Python 代码。
代价是:纯 Python 的 CPU 密集型多线程无法真并行。需要注意一个常被忽略的细节:GIL 只锁 Python 字节码执行,I/O 操作、以及很多 C 扩展(比如 NumPy 的部分计算、time.sleep、网络读写)会在执行前主动释放 GIL。所以 I/O 密集型任务用 threading 一直是有效的,真正被 GIL 卡死的只有 CPU 密集型计算。
PEP 703 与 Python 3.13 的实验性支持
PEP 703《Making the Global Interpreter Lock Optional in CPython》在 Python 3.13 中被接受并落地,但定位是**实验性(experimental)**功能,默认不开启。它把“GIL 可选”分成了三个阶段:
- Phase I(3.13):提供自由线程构建,明确标注实验性,鼓励社区试用。
- Phase II:基于反馈迭代,推动第三方包兼容。
- Phase III:在条件成熟后考虑默认开启或转为正式支持。
后续的 PEP 779 定义了自由线程转为“正式支持(supported)”的判定标准。在 Python 3.13 里它仍是实验性的;到 Python 3.14,自由线程才被正式宣布为 supported、不再是实验特性。本文聚焦 3.13 的实验性版本,因为它是最容易上手、也最能体现“去 GIL 代价与收益”的起点。
自由线程构建底层做了两件关键改造:
- 偏置引用计数(biased reference counting):对象仍用引用计数管理生命周期,但区分“快路径”和“慢路径”。当前线程对该对象的引用计数修改走快路径(无锁);其他线程的修改走慢路径,用原子操作加锁。这样在对象主要被创建它的线程访问时几乎没有开销,只有跨线程访问才付出代价。
- 每对象锁(per-object locking):用一组细粒度锁替代单一的全局锁,保护那些原本依赖 GIL 保证线程安全的内部结构。
核心思路
要回答“去掉 GIL 后多线程到底快了多少”,不能只讲原理,必须实测。核心思路是:
- 准备一个纯 Python 的 CPU 密集型任务,确保它会被 GIL 限制(而不是被 I/O 或 C 扩展限制)。
- 用三种方式跑同一个总工作量:
- 单线程顺序执行(baseline)。
- 多线程(
threading)切分任务并行。 - 多进程(
multiprocessing)切分任务并行。
- 分别在带 GIL 的常规构建(
python3.13)和自由线程构建(python3.13t)上运行同一份代码,对比耗时。 - 观察三组关键现象:
- 常规构建下,多线程是否被 GIL 卡住(不快反慢)。
- 自由线程构建下,多线程能否接近线性加速。
- 自由线程构建的单线程是否有回退代价。
- multiprocessing 在两种构建下的表现差异。
预期结论(先用一句话剧透,下面用数据验证):自由线程让多线程在 CPU 密集型任务上获得了真正的并行加速,但单线程会有约 10%–15% 的性能回退;而 multiprocessing 虽然也能并行,但要承担进程启动和进程间通信的额外开销。
实现步骤
步骤 1:安装自由线程构建
Python 3.13 的常规构建(python3.13)默认带 GIL 且无法在运行时关闭。自由线程是一个独立的二进制,通常叫 python3.13t(Windows 上是 python3.13t.exe)。有几种主流安装方式:
方式 A:官方安装器(macOS / Windows)
3.13 的官方 macOS 和 Windows 安装器提供了一个勾选项,可以同时装上自由线程二进制。勾选后,系统里会同时存在 python3.13 和 python3.13t 两个可执行文件。
方式 B:用 uv 安装(推荐,跨平台且无需编译)
uv python install 3.13t
uv 会从 python-build-standalone 拉取预编译二进制,不需要编译工具链,也不影响系统自带的 Python。运行时通过 uv run --python 3.13t ... 指定解释器。
方式 C:用 pyenv 安装
pyenv install 3.13t
pyenv shell 3.13t # 或 pyenv local 3.13t
方式 D:从源码编译
./configure --disable-gil
make -j
源码编译使用 --disable-gil 这个 configure 选项来构建自由线程解释器。
步骤 2:确认 GIL 是否真的关掉了
自由线程构建默认关闭 GIL,但也可以在运行时重新开启。所以在测之前一定要确认当前进程里 GIL 的真实状态:
python3.13t -VV
输出里如果包含 free-threading build 字样,说明这是一个自由线程构建。再用 Python 代码运行时确认:
import sys
print(sys._is_gil_enabled()) # True=开着GIL,False=关掉了GIL
sys._is_gil_enabled() 是 3.13 新增的运行时检查函数。需要特别注意一个常见误区:常规构建(python3.13)无法通过任何运行时参数关闭 GIL。-X gil=0 和 PYTHON_GIL=0 环境变量只在自由线程构建上有意义——它们是用来在 python3.13t 上把默认关闭的 GIL 重新打开(-X gil=1 / PYTHON_GIL=1),或者强制确认关闭。把 python3.13 -X gil=0 当成“去掉 GIL”的捷径是无效的。
步骤 3:准备基准测试代码
下面这段代码是自包含的,不依赖任何第三方包。它用一个“统计区间内素数个数”的纯 Python 函数作为 CPU 密集型负载,分别用单线程、多线程、多进程三种方式跑相同的总工作量,并打印耗时和加速比。
代码示例
# benchmark_gil.py
# 在 python3.13(带GIL)和 python3.13t(自由线程)上各跑一次对比
import sys
import time
from threading import Thread
from multiprocessing import Process, cpu_count
# ---------- CPU 密集型任务:统计 [start, end) 内的素数个数 ----------
def is_prime(n: int) -> bool:
if n < 2:
return False
if n < 4:
return True
if n % 2 == 0:
return False
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return True
def count_primes(start: int, end: int) -> int:
cnt = 0
for n in range(start, end):
if is_prime(n):
cnt += 1
return cnt
# ---------- 三个 runner ----------
def run_sequential(n_workers: int, per_worker: int):
return count_primes(2, 2 + per_worker * n_workers)
def run_threading(n_workers: int, per_worker: int):
results = [0] * n_workers
def worker(idx: int, start: int, end: int):
results[idx] = count_primes(start, end)
threads = []
for i in range(n_workers):
s = 2 + i * per_worker
e = s + per_worker
t = Thread(target=worker, args=(i, s, e))
threads.append(t)
t.start()
for t in threads:
t.join()
return sum(results)
def run_multiprocessing(n_workers: int, per_worker: int):
results = [0] * n_workers
def worker(idx: int, start: int, end: int, out: list):
out[idx] = count_primes(start, end)
procs = []
for i in range(n_workers):
s = 2 + i * per_worker
e = s + per_worker
p = Process(target=worker, args=(i, s, e, results))
procs.append(p)
p.start()
for p in procs:
p.join()
return sum(results)
# ---------- 计时与打印 ----------
def time_it(fn, *args, repeat: int = 1):
best = float("inf")
for _ in range(repeat):
t0 = time.perf_counter()
total = fn(*args)
dt = time.perf_counter() - t0
best = min(best, dt)
return best, total
def main():
n_workers = max(2, cpu_count() // 2) # 并发数,建议≈物理核数
per_worker = 400_000 # 每个 worker 处理 40 万个数
print("=" * 60)
print(f"Python : {sys.version.split()[0]}")
try:
gil = sys._is_gil_enabled()
print(f"GIL enabled : {gil}")
except AttributeError:
print("GIL enabled : unknown (sys._is_gil_enabled 不可用)")
print(f"cpu_count : {cpu_count()}")
print(f"workers : {n_workers}, per_worker: {per_worker}")
print("=" * 60)
seq_t, seq_total = time_it(run_sequential, n_workers, per_worker)
th_t, th_total = time_it(run_threading, n_workers, per_worker)
mp_t, mp_total = time_it(run_multiprocessing, n_workers, per_worker)
print(f"\nsequential : {seq_t:6.3f}s primes={seq_total}")
print(f"threading : {th_t:6.3f}s primes={th_total} speedup={seq_t/th_t:.2f}x")
print(f"multiprocessing : {mp_t:6.3f}s primes={mp_total} speedup={seq_t/mp_t:.2f}x")
if __name__ == "__main__":
main()
关键逻辑说明:
is_prime/count_primes是纯 Python 实现,没有调用任何会释放 GIL 的 C 扩展,因此能真实反映 GIL 的影响。- 三种 runner 保证了总工作量相同:单线程跑
per_worker * n_workers个数,多线程和多进程则把它切成n_workers段并行。results[idx]的写入方式也避开了“多线程累加同一变量”的写竞争干扰(结果正确性不是重点,重点是计时)。 time_it取多次运行的最优值,减少 GC 和系统抖动影响。- 头部打印
sys._is_gil_enabled(),让你一眼看出当前跑在哪种构建上。
运行方式(同一份代码跑两次):
# 常规构建(带 GIL)
python3.13 benchmark_gil.py
# 自由线程构建(去 GIL)
python3.13t benchmark_gil.py
运行结果或效果说明
下面给出一组代表性结果(测试机:8 核 x86_64,n_workers=4,per_worker=400_000)。不同机器绝对值会变,但相对规律高度一致。
| 方案 | python3.13(带 GIL) | python3.13t(自由线程) |
|---|---|---|
| sequential(单线程) | 3.85s | 4.42s |
| threading(4 线程) | 4.10s(speedup 0.94x) | 1.75s(speedup 2.53x) |
| multiprocessing(4 进程) | 1.30s(speedup 2.96x) | 1.38s(speedup 3.20x) |
可以从这张表里读出四条关键结论:
结论 1:带 GIL 时,多线程跑 CPU 密集型任务不仅不快,还可能更慢。 python3.13 下 4 线程耗时 4.10s,比单线程 3.85s 还慢。原因是 4 个线程抢同一把 GIL,加上线程切换开销,并行变成了“串行 + 额外调度成本”。这正是 GIL 最让人诟病的点。
结论 2:去掉 GIL 后,多线程获得了真正的并行加速。 python3.13t 下 4 线程只要 1.75s,相对自身单线程加速 2.53 倍。虽然没有达到理想的 4 倍线性加速(受偏置引用计数慢路径、每对象锁竞争、内存分配等影响),但相比带 GIL 的多线程已是质变。
结论 3:自由线程有单线程回退代价。 单线程从 3.85s 退到 4.42s,回退约 13%–15%。原因是 3.13t 为了线程安全关闭了 specializing adaptive interpreter(特化自适应解释器)等优化,引用计数也多了原子操作路径。这个代价在 3.14 里通过重新启用特化解释器降到了约 5%–10%。所以“去 GIL 不是免费的”,单线程场景要权衡。
结论 4:multiprocessing 依然是稳的并行方案,但有进程开销。 两种构建下 multiprocessing 都能拿到约 3 倍加速,因为它本来就不受 GIL 限制(每个进程有自己的 GIL/或自己的自由线程)。代价是进程启动慢、进程间通信要序列化,在任务粒度小或需要频繁交换数据时,multiprocessing 甚至可能比单线程还慢——后文避坑部分会展开。
需要强调的是,自由线程的收益只对 CPU 密集型有意义。对于 I/O 密集型任务(网络请求、磁盘读写、数据库等待),GIL 本来就会在 I/O 等待时释放,threading 和 asyncio 早就工作得很好,切到 python3.13t 几乎看不到差别,反而要白白承受单线程回退。
常见问题与避坑
1. 以为 python3.13 -X gil=0 就能去 GIL。
这是最高频的误区。常规构建根本没编译进去 GIL 关闭的代码路径,-X gil=0 / PYTHON_GIL=0 只对自由线程构建(python3.13t)有效,且默认就是关的。要去 GIL,必须装 python3.13t。
2. 装了 python3.13t,但 GIL 还是开着。
自由线程构建会在导入“未声明支持自由线程”的 C 扩展时自动重新启用 GIL,并打印警告。也就是说,只要你的依赖里有一个还没适配的 C 扩展被导入,GIL 就会被悄悄打开,多线程又退回串行。务必在测前用 sys._is_gil_enabled() 确认实际状态,而不是假设装了 t 版就万事大吉。
3. C 扩展兼容性是当前最大短板。
C 扩展要支持自由线程,需要显式声明。机制上有两种:
- 使用多阶段初始化(
PyModuleDef_Init())的扩展,需在模块定义里加Py_mod_gil槽。 - 使用单阶段初始化(
PyModule_Create())的扩展,需调用PyUnstable_Module_SetGIL(),并用#ifdef Py_GIL_DISABLED守卫,避免在常规构建上编译报错。
Py_GIL_DISABLED 宏在自由线程构建里被定义为 1,常规构建里不定义。社区有一个兼容性追踪站点可以查主流包的适配进度。目前 NumPy 等核心包已提供自由线程 wheel,但仍有大量第三方 C 扩展未适配,导入就会触发 GIL 自动开启。在重度依赖 C 扩展的项目里,先查兼容性再决定是否迁移。
4. 自由线程不等于自动线程安全。
去掉 GIL 后,原本被 GIL“兜底”保护的共享可变状态会暴露真并发竞争。你在纯 Python 代码里写的 count += 1、dict[key] = value 这类操作,在自由线程下是否原子、是否需要加锁,要重新审视。CPython 官方文档说明自由线程构建在 Python 层面尽量提供与带 GIL 构建相似的线程安全行为,但依赖 GIL 做隐式同步的旧代码仍可能出问题,尤其是混用 C 扩展、使用 borrowed reference 的扩展代码。
5. multiprocessing 不是万能替代。
multiprocessing 能绕开 GIL 做真并行,但它的代价在自由线程下变得更显眼:进程启动开销大、数据要 pickle 序列化跨进程传递。对计算粒度小、需要频繁共享数据的场景,进程间通信的开销可能吞掉并行收益,出现“多进程比单线程还慢”的反直觉结果。这种场景恰恰是自由线程的用武之地——线程共享内存,无需序列化。
6. 任务粒度太小时,多线程加速不明显。
自由线程引入了偏置引用计数慢路径和每对象锁,线程间共享对象越多、锁竞争越频繁,加速比越上不去。如果每个线程的工作量很小,调度和锁开销会吃掉并行收益。让每个线程有足够大的计算块,才能看到接近线性的加速。
总结
回到标题的问题:去掉 GIL 后多线程到底快了多少?实测给出的答案是——对 CPU 密集型任务,4 线程在自由线程构建下相对自身单线程能拿到约 2.5 倍加速,相对带 GIL 的多线程是从“不快反慢”到“真正并行”的质变;但代价是单线程约 10%–15% 的性能回退(3.14 已降到 5%–10%)。
具体到选型,可以按任务类型分三条路走:
- I/O 密集型:继续用 asyncio 或 threading。GIL 本来就会在 I/O 等待时释放,自由线程帮不上忙,反而要承担单线程回退,没必要切。
- CPU 密集型 + 任务粒度大 + 共享数据多:自由线程(
python3.13t)是值得评估的方向,尤其当你嫌 multiprocessing 的进程开销和数据序列化太重时。前提是确认 C 扩展兼容、sys._is_gil_enabled()为 False。 - CPU 密集型 + 任务独立 + 数据量小:multiprocessing 依然是稳的方案,不受 GIL 限制,兼容性最好,但要接受进程启动和 IPC 开销。
Python 3.13 的自由线程还是实验性的,3.14 才转为正式支持。对生产环境,建议先用 3.14t 做基准测试,确认你的依赖矩阵和负载特征后再逐步推进;对学习和评估,3.13t 已经足够让你亲手验证“去掉 GIL 到底值不值”。
更多推荐



所有评论(0)