
摘要 :Python 3.15 free-threading 拿到稳定 ABI:C 扩展一次编译、跨版本复用。但非默认构建、单线程 1%--8% 回退、C 扩展未适配会"静默重启用 GIL"。本文用公开基准算性能账,给出"要不要上"决策树与
sys._is_gil_enabled()自检.
8 月 4 日,Python 3.15 rc1 发布。朋友圈刷屏的是 lazy imports、frozendict 这些新特性,但我盯着的是另一条不起眼的公告:free-threading 构建拿到稳定 ABI 了。
为什么盯着这条?因为过去两年,我把"Python 后端 + CPU 密集"这个组合翻来覆去地权衡过。线程吃不满多核,多进程内存翻几倍------这笔账,写 Python 后端的人都算过。
先把结论放在前面:abi3t 稳定 ABI(PEP 803)是 free-threading 从"能跑"走向"能上规模"的关键一块基础设施,但它不是免费的午餐。 三个代价------非默认构建、单线程 1%--8% 损耗、C 扩展未适配会静默重启用 GIL(Global Interpreter Lock,全局解释器锁)------决定了它今天只适合一部分服务。下面把账算细。
一、老问题:GIL 拆了,轮子却要反复造
free-threading(无 GIL 构建)从 3.13 就有了实验版。但"有"和"能用"之间隔着一条工程鸿沟:C 扩展。
以前的情况是,每出一个新的 free-threaded 小版本,C 扩展就得重编一次 wheel(发行包)。NumPy、Pandas 这类重型依赖还好,社区追着适配;但长尾的自研扩展、内部依赖呢?每次版本升级都是一轮重新编译、重新验证。对一个要稳定运行的服务来说,这成本不可接受。
PEP 803 的 abi3t 就是冲这个来的:C 扩展按稳定 ABI(Application Binary Interface,应用二进制接口)编译一次,产物可以在多个 free-threaded 小版本之间复用,不用重编。 打个类比------以前是每换一次发动机就得重造整辆车,现在发动机接口标准化了,换个车型还能接着用。

代价也有:想吃到 abi3t,部分 C 扩展需要改写声明,显式承诺"free-thread-safe"。这不是配置开关,是一次性迁移成本。
二、性能账本:值不值,先看数据
讲适用性之前先把数据摆出来。下面这组是公开基准(8 核 EPYC,Python 3.13 --disable-gil 构建,CPU 密集型任务):
| 方案 | 耗时 | 内存 |
|---|---|---|
| 单线程 | 42.8s | 120MB |
| 多进程 | 6.8s | 840MB |
| free-threaded | 5.6s | 145MB |

三个观察:
第一,free-threaded 比多进程还快 18%,内存却只有多进程的不到五分之一。多进程那份 840MB,是进程间数据复制的税。
第二,官方口径下 4 核 CPU 密集场景能拿到 3.1--3.5 倍提速。不是线性,但足够香。
第三,也是最容易被忽略的:单线程有 5%--10% 的回退。 你的服务如果大部分时间单线程跑,上 free-threading 是净亏。1%--8% 还是 5%--10%,不同负载形态数字不同,但方向一致------有代价。
三、最阴的坑:GIL 会"静默"回来
这是我看来全文最重要的一节。
一个 C 扩展如果没声明自己 free-thread-safe,在 free-threaded 构建里导入时,解释器会为整个进程静默重新启用 GIL------只给一条 warning,不报错,不停机。你的服务表面上跑在"无 GIL"构建上,实际上 GIL 回来了,多线程还是串行。
验证方法很简单,导入依赖之后立刻查:
python
import sys
import your_c_extension # 换成你真实的 C 扩展依赖
# 关键一步:导入后确认 GIL 状态,别信构建版本号
print("GIL enabled:", sys._is_gil_enabled())
# 输出 True 就说明有依赖把你拖回了 GIL 模式
我把这套验证做成了一个固定的启动自检。CPU 密集基准的骨架长这样:
python
import sys, time
from concurrent.futures import ThreadPoolExecutor
def compute_heavy_hash(data: bytes) -> int:
acc = 0
for b in data:
acc = (acc * 31 + b) & 0xFFFFFFFF
return acc
print("GIL enabled:", sys._is_gil_enabled()) # 先确认 GIL 真关了
if __name__ == "__main__":
payloads = [bytes(2_000_000) for _ in range(8)]
start = time.perf_counter()
with ThreadPoolExecutor() as pool:
results = list(pool.map(compute_heavy_hash, payloads))
print(f"elapsed: {time.perf_counter() - start:.2f}s")
同一份代码,GIL 开和关,耗时差好几倍。谁跑谁知道。
还有一个真实的竞态案例值得记一笔(公开案例,来源见文末环境说明):某电商风控计数器从 GIL 构建迁到 free-threaded 后,命中量偏低 5--10%。排查下来根因是 counter += 1 不是原子操作------GIL 时代它"碰巧"安全,现在真并发了,丢更新就成了日常。修复要么显式加锁,要么用 threading.local()。
GIL 拆掉的那一刻,你十年的多线程直觉也跟着拆了一半。
四、要不要上:一棵决策树
综合上面的账,我把判断逻辑整理成了一棵决策树,照着走就行:

翻译成文字版:
- 服务是不是 CPU 密集的并行热点? 不是------比如 IO 密集、异步已经够用------那 GIL 本来就不是你的瓶颈,别动。
- 所用的 C 扩展是否已适配 free-thread-safe? 没适配------要么等社区适配,要么接受静默 GIL(那就等于白迁)。
- 单线程 1%--8% 的回退能不能接受? 不能------比如延迟敏感的单线程路径,那再等等。
三关都过,再上 no-gil 构建,并且上线第一件事就是跑 sys._is_gil_enabled() 自检。
据我的观察,今天能三关全过的服务,是那种"纯 Python 计算逻辑 + 依赖都健康"的类型------数据处理管道、批量计算服务这类。典型 Web 服务?先让子弹飞一会儿。
五、时间线与配套
补一下版本节奏,方便排期:rc1 已于 8 月 4 日发布(ABI 已锁定),rc2 预计 9 月 1 日,final 预计 10 月 1 日(PEP 790)。另外 3.15 自带了一个采样分析器 Tachyon(PEP 799),stdlib 内置,能 attach 到运行中的进程,最高 1MHz 采样------判断"该不该上"之前,先用它看看热点到底在哪。
写在最后
free-threading 稳定 ABI 解决的是"规模化采用"的最后一公里:编译一次、跨版本复用。但 Python 生态真正补完这门课,还要等长尾 C 扩展逐个适配。
我的建议很简单:现在就把 sys._is_gil_enabled() 自检和那套基准脚本放进你的工具箱,新版本出来跑一遍,用数据决定上不上,而不是用热情。
你的服务是 CPU 密集型吗?C 扩展依赖清点过了没?评论区聊聊你的判断。
环境说明:Python 3.15rc1 / 3.14t 对照;基准数据来自公开测试(8 核 EPYC),竞态案例来自公开技术复盘,已在文中标注;决策树与配图为本仓库 resvg 本地渲染。
标签:Python3.15 / free-threading / GIL / 高并发 / 稳定ABI