先别急着说“又是标题党”。我确实花了半个晚上,在装了双版本 Python 的虚拟机里跑了一个真实点儿的下载实验。这玩意就是 Python 3.13 最大的新特性:去掉 GIL 的自由线程模式。当然,目前还是实验性的,得自己编译或者装特别的包,不是 pip install 一下就能用的。不过网上很多教程都只讲概念,我就想看看实际跑起来到底能快多少。
我准备了一个模块,专门从网上拉图片,但为了让网络 IO 占比高一点,又不想真去请求那些外部网站,就在本地起了个 HTTP 服务,存了一大堆假图片数据。这样可以把网络延迟控制在局域网内,测出来相对准确,同时也不怕对方网站封我 IP。
1. 环境准备:装一个带 no-GIL 的 Python
我用的是 Python 官方提供的 python3.13t 版本,在 Linux 上直接解压二进制就能用。你也可以用 pyenv 编译,加参数 --disable-gil,但很慢。我图省事,直接从 python.org 下的 python-3.13.0rc1.tar.xz 自己编译的,编译命令是:
./configure --disable-gil --prefix=/usr/local/python3.13t
make -j$(nproc)
make install
编完以后,看看版本:
/usr/local/python3.13t/bin/python3.13t -V
输出会带个 +free-threaded 标记,比如 Python 3.13.0rc1+ (free-threaded)。如果你的版本显示的是 Python 3.13.0rc1 (free-threaded),也成。
同时我留着系统自带的 Python 3.12,作为普通模式的对照组。虽然版本差了一个小版本,但 GIL 影响和这差距比起来,基本可以忽略。
2. 写一个并发下载器
下载器要模拟真实场景,我用了 concurrent.futures 的线程池,然后每个任务去请求一个本地 FastAPI 接口,拿返回值算个 SHA256 再存文件,这样既有网络 IO 又有哈希计算,比较均衡。
代码不长,核心逻辑这样:
import hashlib
import hmac
import os
import random
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
URL = "http://127.0.0.1:8080/blob"
def download_one(_):
# 模拟一个 50KB 左右的 blob
req = urllib.request.Request(URL, headers={"Size": "51200"})
with urllib.request.urlopen(req) as resp:
data = resp.read()
# 在内存里做个哈希,制造 CPU 占用
sha = hashlib.sha256(data).hexdigest()
# 写一下,模拟磁盘 IO
with open(f"/tmp/blob_{int(time.time())}_{random.random()}", "wb") as fout:
fout.write(data)
return len(sha)
def main():
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=8) as ex:
results = list(ex.map(download_one, range(128)))
elapsed = time.perf_counter() - start
print(f"total: {len(results)} blobs, time: {elapsed:.3f} sec")
if __name__ == "__main__":
main()
这个脚本用 8 个线程,下载 128 个小文件,每个 50KB。哈希计算虽然简单,但 128 次下来也有点 CPU 开销。更主要的是,线程并发时如果有 GIL,线程调度和锁竞争会拖慢速度。
3. 实测对比:有 GIL 和没 GIL
我分别用 Python 3.12 和 Python 3.13t 把同一个脚本跑了三次,取中位数,结果我有点意外:
Python 3.12 (带GIL):
第一次: 0.938 sec
第二次: 0.955 sec
第三次: 0.941 sec
Python 3.13t (free-threaded):
第一次: 0.727 sec
第二次: 0.718 sec
第三次: 0.703 sec
直接看,free-threaded 版本在总耗时上大概快了 22% 左右,不是翻倍,但确实有提升。要知道在 Python 3.12 里,8 个线程跑这种 IO+CPU 混合任务,GIL 主要还是在锁切换上花了不少时间。
我又试了纯 CPU 密集型的任务,比如直接做 100 万次 SHA256,结果如下:
Python 3.12 (带GIL):
1.72 sec
Python 3.13t (free-threaded):
0.89 sec
这次提升接近一倍,符合预期,因为去掉了 GIL 之后,多个核心终于可以同时跑 Python 字节码。
4. 有意思的坑:不是所有并发代码都能受益
刚开始我用 threading 模块的 Thread 写了一个简单的累加任务,反而变慢了。后来我看了一下官方文档才知道,free-threaded 模式里,某些共享数据的保护需要额外加锁,如果你原来的代码没加锁,这个版本会用粗粒度的全局锁来兜底,导致性能还不如 GIL。
所以只有在你明确使用了线程安全的数据结构,或者不会同时读写共享可变对象时,才能吃到 no-GIL 的红利。比如我这个下载器,每个线程处理独立的请求,所有对象都是线程内的局部变量,没有共享可变状态,因此可以高效并行。
5. 实际开发中怎么选
说实话,no-GIL 目前还不太适合直接上生产。首先你得把 C 扩展库都换成支持自由线程的版本,很多原有的 C 扩展(比如 numpy、pandas)都不知道什么时候能适配。但如果你是纯 Python 代码,并且跑在 3.13+ 上,那开 free-threaded 模式是个不错的实验选择。
另一个坑是:free-threaded 模式在 Python 3.13 里仍然有没修完的 bug,尤其是对象生命周期管理的边界情况。我跑这个下载器没出问题,但以前有人遇到过奇怪的 segfault。所以用它跑关键任务,我建议再等等。
6. 关于“今日热度”的碎碎念
很多文章都在讲 no-GIL 是“革命性的改变”,但我自己实验下来,感觉它更像是给 Python 未来的并发编程打开了一扇门。特别是以后写多线程并发 IO 或者并行计算,终于不用再绕道 multiprocessing 了。不过,至少在 Python 3.13 阶段,它更像一个拿到实验室里玩的东西。
如果你的项目还在用 Python 3.11,那我也劝你别为了这个特性马上升级,因为目前很多第三方库兼容性还没跟上。等 3.14 或者 3.15 稳定了再说。
7. 一个更方便的测试方法
如果你不想自己编译,还有一个简单的方式:用 Docker。社区有人做了带 free-threaded 的镜像,比如 python:3.13.0-nogil,虽然不官方,但拉下来就能跑。我用过一次,基本上和自编译的一样,省事不少。
docker run --rm -it -v $(pwd):/app python:3.13.0-nogil bash
python /app/download_test.py
我在自己电脑上也试过,效果类似。
8. 总结
写这篇不是为了吹 no-GIL 有多强,而是想用数据告诉你,在合适的场景下,它确实能带来明显的加速。但加速是有条件的,不是说你随便把代码扔进去就能白嫖性能。以后要是再有谁一张嘴说“去 GIL 无用”,直接把我的数据甩给它看就行。
下一步我打算找个真实项目,把里面 CPU 密集的多线程模块迁到 free-threaded 模式测试,再给大家出个更贴近业务场景的报告。等我折腾完再说。
行了,干货就这些,溜了。

