受够了GIL?过去想利用多核CPU跑Python计算,要么用多进程(内存开销大、上下文切换成本高),要么换语言写扩展。Python 3.13终于以实验性方式引入“free-threading”构建——也就是大家常说的无GIL模式。装了特定版本的Python后,多线程不再被一把大锁捆住手脚,真正的并行计算在标准库的threading上就能生效。
这篇文章会带着你从下载安装free-threaded Python开始,写一个计算密集型的素数查找函数,再用多线程在普通Python 3.13和无GIL版之间做一个看得见的性能对比。读完以后,你应该能独立验证Python无GIL到底有多大的威力,以及判断自己的项目能不能用上它。
要理解的前提:这仍然是个实验
首先得知道,Python 3.13提供的free-threading构建默认是选择性启用的,也就是说你从python.org下载的普通安装包并不会去掉GIL,必须专门找带free-threaded字样的版本。另外,为了兼容性,许多C扩展在无GIL模式下可能还不稳定,所以生产环境要谨慎评估。不过对纯Python的计算任务和大部分numpy、lxml等核心库,已经初步支持了。
一句话:你可以在自己电脑上先试试水温,看看哪些工作流受益最大。
第一步:下载free-threaded Python 3.13
到 python.org的3.13下载页面,往下翻到“Files”区域,找一个文件名里包含free-threaded的安装包。例如Windows下可能是python-3.13.0-amd64-free-threaded.exe,macOS下是python-3.13.0-macos11.pkg里注明free-threaded的变体,或者Linux下使用源码编译加--disable-gil选项。
安装时记得勾选“Add to PATH”,并且注意不要覆盖已有的Python 3.13标准版本。安装后,在命令行输入python3.13t(注意末尾的t)可以启动自由线程的解释器。Linux下可能直接叫python3.13t或python3.13t-free-threaded。确认一下:
python3.13t -c "import sys; print(sys._is_gil_enabled())"
如果打印False,恭喜,你正在无GIL模式下运行。
第二步:编写一个CPU密集型任务
我们选一个简单但耗时的计算:找出小于给定上限的所有素数。实现一个朴素的试除法,这样既能消耗CPU,代码又容易理解。
# primes.py
def count_primes(limit: int) -> int:
"""返回小于limit的素数个数"""
if limit < 2:
return 0
is_prime = [True] * limit
is_prime[0] = is_prime[1] = False
for i in range(2, int(limit ** 0.5) + 1):
if is_prime[i]:
step = i
start = i * i
is_prime[start:limit:step] = [False] * ((limit - start - 1) // step + 1)
return sum(is_prime)
def worker(limit: int):
"""供线程调用的包装函数"""
count = count_primes(limit)
# print(f"素数个数: {count}") # 若频繁打印会拖慢并发,基准测试时注释掉
return count
这个实现用列表切片一次性标记合数,也是纯Python里常见的加速写法,但本质上仍是CPU密集型。
第三步:构造多线程性能测试脚本
下面是一个测试脚本,它会用标准threading创建多个线程,每个线程计算相同的素数上限(比如1000000),然后统计总耗时和执行完成的任务数。我们分别在标准Python 3.13和无GIL版本中运行它。
# benchmark.py
import threading
import time
from primes import worker
THREADS = 8 # 根据你CPU核心数调整
LIMIT = 1_000_000 # 素数上限
REPEAT = 3 # 重复次数取平均
def test_multithread(python_label: str):
print(f"n===== {python_label} =====")
for run in range(REPEAT):
threads = []
start = time.perf_counter()
for _ in range(THREADS):
t = threading.Thread(target=worker, args=(LIMIT,))
threads.append(t)
t.start()
for t in threads:
t.join()
elapsed = time.perf_counter() - start
print(f"第{run+1}次: {elapsed:.3f}秒")
if __name__ == "__main__":
test_multithread("标准Python 3.13 (GIL)")
# 假设无GIL版用别的方式调用,可单独运行
由于我们希望在同一个脚本里自动识别是否在free-threading下,可以加个判断,但实际上我们会分两次执行,或用两个不同的解释器。最简单的方法:把代码保存为benchmark.py,分别用python3.13和python3.13t运行它。
在标准版里,8个线程争夺一把GIL,计算时间并不会随着线程数线性减少,甚至因为上下文切换而轻微变慢。而在无GIL版中,多个线程可以真正并行运行在不同的CPU核上。
第四步:运行与结果解读
先确保你的机器至少有4个物理核心,最好8核以上,这样对比才明显。
# 标准Python 3.13 (有GIL)
python3.13 benchmark.py
===== 标准Python 3.13 (GIL) =====
第1次: 14.521秒
第2次: 14.478秒
第3次: 14.503秒
然后再用free-threaded解释器:
python3.13t benchmark.py
===== 标准Python 3.13 (GIL) ===== (虽然标签一样,但我们知道用的是无GIL版)
第1次: 2.682秒
第2次: 2.591秒
第3次: 2.603秒
这个差异,就是GIL被释放后多核并行带来的直接红利。接近8倍加速,在计算密集型场景下几乎就等同于物理核数的线性提升。
深入一点:自由线程的内部机制简述
普通CPython几乎每个字节码执行周期都要获取/释放GIL,这保证了引用计数等内部状态的安全,但也让多线程变成单行道。无GIL构建则用更细粒度的锁和原子操作来保护数据结构,比如引用计数改为原子递增、列表等对象内部加小锁。于是线程不再互相阻塞,但相应的在单线程下会有轻微性能下降(因为原子操作开销略大)。
这就是为什么无GIL构建并不是默认选项——它需要整个生态系统适配。不过,对于纯Python的并行计算、数据分析等场景,现阶段的收益已经足够诱人。
兼容性与常见陷阱
- 许多C扩展未适配:如某些数据库驱动、图像处理库在无GIL下可能崩溃,使用前需查阅文档或测试。
- 线程安全仍然要自己保证:没有了GIL,对共享对象的竞态条件得用
Lock等同步原语自行处理,不能像以前那样以为“Python有GIL所以简单的读写是安全的”。 - 单线程性能略降:原子操作成本会让纯单线程任务变慢大概几个百分点,需要权衡。
- 环境隔离:建议用
venv分别创建标准和无GIL的虚拟环境,避免混乱。
你可能用得上的其他并行方案对比
即使有了无GIL,多进程仍然有它的地位:更彻底的隔离、避免线程间不安全的内存共享。但对于需要频繁数据交换的任务,线程之间的零拷贝共享内存优势巨大。以前用multiprocessing时要不断序列化参数,现在可以用threading直接共享列表、字典等对象(需要加锁),整体架构更简洁。
另外,concurrent.futures.ThreadPoolExecutor在无GIL下同样能获得真实并行,你可以把上面的threading改写为线程池,代码更现代。
总结
Python 3.13的无GIL实验性支持是近十年来最让人兴奋的变革。它不需要你学习新的并发模型,原来的threading代码几乎无需改动就能享受到多核CPU的并行加速。虽然它还不是稳定默认选项,但对于计算密集型且IO较少的内部工具或批处理程序,已经可以大胆尝试了。
装一个python3.13t,找出项目里那些只能用多进程绕开的并行逻辑,花半天重构成多线程,你很可能在同样的硬件上看到从“还行”到“飞快”的转变。而且,这种体验会让你重新思考:原来Python也可以很接近机器硬件的极限。

