Python 3.13 终于能去掉 GIL 了?自由线程实测:多线程到底快了多少

📅 2026/7/9 1:07:51
Python 3.13 终于能去掉 GIL 了?自由线程实测:多线程到底快了多少
前言写过 Python 多线程的人大概都经历过一种困惑明明机器有 8 个核开了 8 个线程跑 CPU 密集型任务速度却和单线程差不多甚至更慢。罪魁祸首大家都听过——GIL全局解释器锁。多年来Python 社区给出的标准答案是“CPU 密集型请用 multiprocessingI/O 密集型才用 threading”。Python 3.13 改变了这件事的剧本。它通过 PEP 703 引入了一个实验性的“自由线程”free-threading构建模式把 GIL 真正关掉。这听起来像是一个迟到了二十年的礼物但去掉 GIL 到底能快多少、要付出什么代价、现在能不能上生产这些问题不能靠想象回答。本文会从 GIL 的原理讲起再用一段可运行的基准测试代码把自由线程、传统 GIL、multiprocessing 三者放在一起实测对比并给出该不该用、怎么用的选型结论。背景或问题GIL 是什么为什么它卡住了多线程GILGlobal 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 I3.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.13tWindows 上是python3.13t.exe。有几种主流安装方式方式 A官方安装器macOS / Windows3.13 的官方 macOS 和 Windows 安装器提供了一个勾选项可以同时装上自由线程二进制。勾选后系统里会同时存在python3.13和python3.13t两个可执行文件。方式 B用 uv 安装推荐跨平台且无需编译uv pythoninstall3.13tuv 会从 python-build-standalone 拉取预编译二进制不需要编译工具链也不影响系统自带的 Python。运行时通过uv run --python 3.13t ...指定解释器。方式 C用 pyenv 安装pyenvinstall3.13t pyenv shell3.13t# 或 pyenv local 3.13t方式 D从源码编译./configure --disable-gilmake-j源码编译使用--disable-gil这个 configure 选项来构建自由线程解释器。步骤 2确认 GIL 是否真的关掉了自由线程构建默认关闭 GIL但也可以在运行时重新开启。所以在测之前一定要确认当前进程里 GIL 的真实状态python3.13t-VV输出里如果包含free-threading build字样说明这是一个自由线程构建。再用 Python 代码运行时确认importsysprint(sys._is_gil_enabled())# True开着GILFalse关掉了GILsys._is_gil_enabled()是 3.13 新增的运行时检查函数。需要特别注意一个常见误区常规构建python3.13无法通过任何运行时参数关闭 GIL。-X gil0和PYTHON_GIL0环境变量只在自由线程构建上有意义——它们是用来在python3.13t上把默认关闭的 GIL 重新打开-X gil1/PYTHON_GIL1或者强制确认关闭。把python3.13 -X gil0当成“去掉 GIL”的捷径是无效的。步骤 3准备基准测试代码下面这段代码是自包含的不依赖任何第三方包。它用一个“统计区间内素数个数”的纯 Python 函数作为 CPU 密集型负载分别用单线程、多线程、多进程三种方式跑相同的总工作量并打印耗时和加速比。代码示例# benchmark_gil.py# 在 python3.13带GIL和 python3.13t自由线程上各跑一次对比importsysimporttimefromthreadingimportThreadfrommultiprocessingimportProcess,cpu_count# ---------- CPU 密集型任务统计 [start, end) 内的素数个数 ----------defis_prime(n:int)-bool:ifn2:returnFalseifn4:returnTrueifn%20:returnFalsei3whilei*in:ifn%i0:returnFalsei2returnTruedefcount_primes(start:int,end:int)-int:cnt0forninrange(start,end):ifis_prime(n):cnt1returncnt# ---------- 三个 runner ----------defrun_sequential(n_workers:int,per_worker:int):returncount_primes(2,2per_worker*n_workers)defrun_threading(n_workers:int,per_worker:int):results[0]*n_workersdefworker(idx:int,start:int,end:int):results[idx]count_primes(start,end)threads[]foriinrange(n_workers):s2i*per_worker esper_worker tThread(targetworker,args(i,s,e))threads.append(t)t.start()fortinthreads:t.join()returnsum(results)defrun_multiprocessing(n_workers:int,per_worker:int):results[0]*n_workersdefworker(idx:int,start:int,end:int,out:list):out[idx]count_primes(start,end)procs[]foriinrange(n_workers):s2i*per_worker esper_worker pProcess(targetworker,args(i,s,e,results))procs.append(p)p.start()forpinprocs:p.join()returnsum(results)# ---------- 计时与打印 ----------deftime_it(fn,*args,repeat:int1):bestfloat(inf)for_inrange(repeat):t0time.perf_counter()totalfn(*args)dttime.perf_counter()-t0 bestmin(best,dt)returnbest,totaldefmain():n_workersmax(2,cpu_count()//2)# 并发数建议≈物理核数per_worker400_000# 每个 worker 处理 40 万个数print(*60)print(fPython :{sys.version.split()[0]})try:gilsys._is_gil_enabled()print(fGIL enabled :{gil})exceptAttributeError:print(GIL enabled : unknown (sys._is_gil_enabled 不可用))print(fcpu_count :{cpu_count()})print(fworkers :{n_workers}, per_worker:{per_worker})print(*60)seq_t,seq_totaltime_it(run_sequential,n_workers,per_worker)th_t,th_totaltime_it(run_threading,n_workers,per_worker)mp_t,mp_totaltime_it(run_multiprocessing,n_workers,per_worker)print(f\nsequential :{seq_t:6.3f}s primes{seq_total})print(fthreading :{th_t:6.3f}s primes{th_total}speedup{seq_t/th_t:.2f}x)print(fmultiprocessing :{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()让你一眼看出当前跑在哪种构建上。运行方式同一份代码跑两次# 常规构建带 GILpython3.13 benchmark_gil.py# 自由线程构建去 GILpython3.13t benchmark_gil.py运行结果或效果说明下面给出一组代表性结果测试机8 核 x86_64n_workers4per_worker400_000。不同机器绝对值会变但相对规律高度一致。方案python3.13带 GILpython3.13t自由线程sequential单线程3.85s4.42sthreading4 线程4.10sspeedup 0.94x1.75sspeedup 2.53xmultiprocessing4 进程1.30sspeedup 2.96x1.38sspeedup 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 不是免费的”单线程场景要权衡。结论 4multiprocessing 依然是稳的并行方案但有进程开销。两种构建下 multiprocessing 都能拿到约 3 倍加速因为它本来就不受 GIL 限制每个进程有自己的 GIL/或自己的自由线程。代价是进程启动慢、进程间通信要序列化在任务粒度小或需要频繁交换数据时multiprocessing 甚至可能比单线程还慢——后文避坑部分会展开。需要强调的是自由线程的收益只对 CPU 密集型有意义。对于 I/O 密集型任务网络请求、磁盘读写、数据库等待GIL 本来就会在 I/O 等待时释放threading 和 asyncio 早就工作得很好切到python3.13t几乎看不到差别反而要白白承受单线程回退。常见问题与避坑1. 以为python3.13 -X gil0就能去 GIL。这是最高频的误区。常规构建根本没编译进去 GIL 关闭的代码路径-X gil0/PYTHON_GIL0只对自由线程构建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 到底值不值”。