bigywater.tech
Python · 91

CPython free-threading:去掉 GIL 后会改变什么

自由线程(free-threading)是 Python 3.13 起出现的实验性构建:去掉 GIL,让多个线程真正同时运行 Python 字节码。这篇装了官方的 3.14t,拿同样的代码在三种配置下比:有 GIL、无 GIL、以及无 GIL 的构建里再把 GIL 打开。

1.00x → 2.61x
4 个线程做纯 Python 计算时,GIL 版与自由线程版的加速比
843,173
自由线程下 4 线程 × 50 万次 += 1 的结果(期望 2,000,000)
PYTHON_GIL=1
同一个 3.14t,重新打开 GIL 后加速比回到 1.2x 左右

做的事:三种解释器配置

实测 · 本机执行输出
  普通 3.14(有 GIL)                     3.14.7 | Py_GIL_DISABLED = 0 | sys._is_gil_enabled() = True | abiflags =
  3.14t(自由线程)                        3.14.7 | Py_GIL_DISABLED = 1 | sys._is_gil_enabled() = False | abiflags = t
  3.14t + PYTHON_GIL=1(重新打开 GIL)     3.14.7 | Py_GIL_DISABLED = 1 | sys._is_gil_enabled() = True | abiflags = t

普通 3.14 的 Py_GIL_DISABLED 是 0、GIL 开;3.14t 是 1、GIL 关(sys.abiflags 是 t);同一个 3.14t 用环境变量 PYTHON_GIL=1 还能在运行时把 GIL 重新打开,sys._is_gil_enabled() 就会变回 True。3.14t 是我用 uv python install 3.14t 装在临时目录里的,没有动系统的 Python。

验证一:CPU 密集任务,线程到底能不能加速

每个线程做的事 def work(): s = 0 for i in range(3_000_000): s += i * i
实测 · 本机执行输出
  机器有 10 个核
  普通 3.14(有 GIL)
     线程数 1: 串行   104.2 ms   线程并行    96.9 ms   加速比 1.08x
     线程数 2: 串行   193.2 ms   线程并行   194.2 ms   加速比 0.99x
     线程数 4: 串行   396.3 ms   线程并行   396.3 ms   加速比 1.00x
     线程数 8: 串行   791.0 ms   线程并行   785.5 ms   加速比 1.01x
  3.14t(自由线程)
     线程数 1: 串行   101.1 ms   线程并行    89.7 ms   加速比 1.13x
     线程数 2: 串行   203.2 ms   线程并行    91.4 ms   加速比 2.22x
     线程数 4: 串行   406.0 ms   线程并行   155.4 ms   加速比 2.61x
     线程数 8: 串行   812.6 ms   线程并行   222.3 ms   加速比 3.66x
  3.14t + PYTHON_GIL=1(重新打开 GIL)
     线程数 1: 串行    95.5 ms   线程并行    89.6 ms   加速比 1.07x
     线程数 2: 串行   226.5 ms   线程并行   179.3 ms   加速比 1.26x
     线程数 4: 串行   443.3 ms   线程并行   360.9 ms   加速比 1.23x
     线程数 8: 串行   891.2 ms   线程并行   717.6 ms   加速比 1.24x

有 GIL 的 3.14 里,不论 2、4、8 个线程,并行耗时都和串行几乎一样(加速比约 1.0x)——同一时刻只有一个线程在执行字节码。自由线程版 4 个线程是 2.61x、8 个线程 3.66x:真的并行了,但没达到线性(本机 10 核,还有线程创建、内存分配、缓存一致性等开销)。设了 PYTHON_GIL=1 之后,同一个 3.14t 也只剩约 1.2x,可见提升来自去掉 GIL。这里的数字是这台机器上这个特定任务的结果,不是通用倍数。

顺带一个和常见说法不太一样的观察:这次单线程的串行耗时,3.14t(约 101 ms)并不比普通 3.14(约 104 ms)慢。这是这个简单循环的结果,别的代码(尤其是大量小对象操作)我没有测,不能推广。

验证二:代价——原来靠 GIL 才「碰巧安全」的代码会出问题

实测 · 本机执行输出
  普通 3.14(有 GIL)                     [2000000, 2000000, 2000000, 2000000, 2000000]
  3.14t(自由线程)                        [843173, 840998, 829967, 846472, 827229]
  3.14t + PYTHON_GIL=1(重新打开 GIL)     [2000000, 2000000, 2000000, 2000000, 2000000]

4 个线程各对同一个 box[0] 做 50 万次 += 1,期望 2,000,000。有 GIL 的 3.14 五次都精确;自由线程版五次都只有 83 万上下,丢了一半以上的更新。+= 1 本来就不是原子操作(读、加、写三步),只是在 GIL 下线程切换点很少,恰好没出事。去掉 GIL 之后,这类隐藏的竞态就暴露了——共享的可变状态要自己加锁(threading.Lock)。

另外我在一次单独的试跑里(输出没有放进上面的终端块)测了 4 个线程各 list.append 20 万次,两种构建的最终长度都是 800000,没有丢——单个内置容器的操作本身是安全的,不安全的是「读—改—写」这类组合动作。

分步演示:加速比,以及代价

数据来自上面的实测
GIL 开 / 关 对线程加速的影响
当前配置

    本文用到的工具与边界

    • 本机 macOS 27 / arm64,10 核;Python 3.14.7(Homebrew)与 3.14.7 free-threaded(uv 安装);每项取 3 次最小值,计时用 time.perf_counter
    • 没有验证:C 扩展在自由线程下的兼容(导入未声明支持的扩展会自动重开 GIL,这条我没有实测)、内存占用差异、I/O 密集任务、其他版本的自由线程性能
    京ICP备2026067502号-1京公网安备11010502063487号