自由线程(free-threading)是 Python 3.13 起出现的实验性构建:去掉 GIL,让多个线程真正同时运行 Python 字节码。这篇装了官方的 3.14t,拿同样的代码在三种配置下比:有 GIL、无 GIL、以及无 GIL 的构建里再把 GIL 打开。
普通 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。
每个线程做的事 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)慢。这是这个简单循环的结果,别的代码(尤其是大量小对象操作)我没有测,不能推广。
普通 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,没有丢——单个内置容器的操作本身是安全的,不安全的是「读—改—写」这类组合动作。
time.perf_counter