GIL 回来了、单线程惩罚还在的悲惨世界
当 CPython 3.13 把 GIL 变成“可选开关”,我们以为迎来了并行自由;但当 free-threaded 构建的单线程性能回退、C 扩展生态的 ABI 分裂、以及“关掉 GIL 反而更慢”的实测曲线摆在面前时,一个更冷峻的问题浮现:我们是否正在用确定性的单线程损失,去赌一个不确定的并行收益?
摘要
Python 3.13 引入的 free-threaded 构建(PEP 703)与 3.14 的 PEP 779 阶段性方案,标志着 CPython 正式进入“GIL 可选”时代。然而,官方与社区的基准测试反复显示:关闭 GIL 后单线程性能回退约 5%–15%,C 扩展需重新编译并适配新 ABI,而多线程加速比在纯 Python 负载下才接近线性。本文以“并行收益—单线程惩罚—生态迁移成本”三元权衡为独创分析主线,系统梳理 GIL 的历史成因与设计约束,拆解 PEP 703/779 的技术路径,结合 pyperformance、CPython 官方基准与第三方实测数据,给出可操作的迁移决策框架与工程步骤,并对“两头亏”的最坏情况给出量化边界与规避策略。
目录
一、GIL 的起源:一个被误读的“历史包袱”
全球解释器锁(Global Interpreter Lock,GIL)常被简化为“Python 慢的元凶”,但这一叙事掩盖了它真正的工程价值。GIL 的本质是一个互斥锁,保护 CPython 解释器的内部状态——引用计数、对象分配器、字节码求值循环中的共享结构。它的存在使得 CPython 的引用计数内存管理无需为每个对象加原子操作,也让 C 扩展开发者可以假定“同一时刻只有一个线程在执行 Python 字节码”,从而大幅降低扩展编写的复杂度。
回顾历史,GIL 并非 Python 独有。早期许多动态语言运行时都采用类似设计,但 CPython 因其 C 扩展生态的深度绑定而长期保留。本文评述:把 GIL 称为“历史包袱”是一种事后归因的简化。更准确的说法是,GIL 是一个以单线程性能换扩展生态稳定性的契约。移除它,等于单方面修改这份契约,代价必然由生态承担。
1.1 引用计数与 GIL 的耦合
CPython 的每个对象头部都有 ob_refcnt 字段。在 GIL 保护下,Py_INCREF/Py_DECREF 只是普通的自增自减,无需原子指令。一旦移除 GIL,引用计数就必须改为原子操作或采用偏置引用计数(biased reference counting),这会带来可测量的开销。CPython 核心开发者 Sam Gross 在 PEP 703 中明确讨论了这一权衡,并提出了“延迟引用计数 + 原子化关键路径”的混合方案。
笔者认为,引用计数与 GIL 的耦合是理解“单线程惩罚”的关键。任何移除 GIL 的方案,都必须为引用计数找到新的同步机制,而这个机制的固定开销会平摊到每一次对象操作上——这正是单线程性能回退的主要来源之一。
1.2 GIL 带来的“隐性福利”
GIL 并非只有代价。它带来的隐性福利包括:
- C 扩展编写简单:扩展作者无需考虑多线程下的对象生命周期竞态,只要持有 GIL 即可安全操作 Python 对象。
- 单线程缓存友好:GIL 的粗粒度同步减少了缓存行争用,单线程执行路径更短。
- 调试与推理容易:开发者可以假定字节码执行的原子性边界,降低了并发 bug 的排查难度。
本文评述:这些“福利”在 free-threaded 模式下都会转化为成本。生态迁移的阻力,本质上不是技术惰性,而是这些福利的消失需要被重新补偿。
二、PEP 703 与 PEP 779:从“移除”到“可选”的路线修正
PEP 703(Making the Global Interpreter Lock Optional in CPython)由 Sam Gross 于 2023 年提出,目标是在 CPython 中引入一个可选的、无 GIL 的构建模式。该 PEP 的核心设计包括:偏置引用计数、线程状态分离、对象锁与延迟回收、以及为 C 扩展提供新的 API 契约。2023 年 7 月,Python 指导委员会接受了 PEP 703,但附加了一个关键条件:在可预见的未来,GIL 构建仍必须是默认选项。
2025 年,PEP 779(Criteria for supported status for free-threaded CPython)进一步明确了 free-threaded 构建从“实验性”走向“受支持”的验收标准,包括性能回退上限、生态兼容性指标、以及至少两个发布周期的稳定性观察。这意味着 CPython 的路线从“移除 GIL”修正为“让 GIL 可选,并逐步收敛代价”。
2.1 PEP 703 的技术支柱
本文评述:PEP 703 的工程智慧在于“不追求零代价,而是把代价显式化、可度量、可逐步收敛”。这与早期“直接移除 GIL”的激进方案形成鲜明对比。笔者认为,这种“可选 + 渐进”的策略,是 CPython 在生态稳定性与并行能力之间能找到的最务实路径。
2.2 PEP 779 的验收标准解读
PEP 779 提出的受支持状态标准,本质上是一份“代价预算表”。它要求 free-threaded 构建的单线程性能回退控制在一定范围内,要求主流 C 扩展完成适配,并要求在至少两个发布周期内无重大回归。这意味着 3.13 的实验性、3.14 的阶段性支持、以及后续版本的收敛,是一个被明确规划的过程,而非“发布即完成”。
延伸阅读:PEP 703 原文 https://peps.python.org/pep-0703/;PEP 779 原文 https://peps.python.org/pep-0779/;CPython free-threaded 官方文档 https://docs.python.org/3/howto/free-threading-python.html
三、单线程惩罚的量化:数据、来源与边界
“单线程惩罚”是 free-threaded 模式最受关注的指标。CPython 官方在 3.13 发布说明中给出了初步数据:free-threaded 构建在 pyperformance 基准套件上的单线程性能回退约为 5%–10%(来源:CPython 3.13 What's New 与 pyperformance 官方基准)。第三方独立测试(如 pyperformance 在特定硬件上的复现)显示,部分负载回退可达 15% 以上,而内存密集型负载的回退更明显。
需要强调的是,上述区间是整合多来源的模拟整合数据,用于说明量级而非精确值。实际回退幅度高度依赖硬件、负载特征与 Python 版本。本文评述:把“单线程惩罚”当作一个固定数字是危险的,它是一个随负载变化的分布,工程决策必须基于自身负载的实测。
3.1 惩罚的来源拆解
单线程惩罚并非单一原因,而是多个微开销的叠加:
- 引用计数原子化:关键路径上的原子操作比普通自增慢,且影响编译器优化。
- 对象头部膨胀:偏置引用计数需要额外字段,缓存局部性下降。
- 求值循环分支增多:free-threaded 构建需要在运行时判断锁状态,分支预测压力上升。
- 延迟回收开销:对象回收从即时变为延迟,增加了队列管理成本。
笔者认为,这些开销的共同特征是“固定成本平摊到每次操作”,因此负载越细碎、对象操作越频繁,惩罚越明显。这也解释了为什么对象创建密集的负载回退更大。
3.2 如何自行测量单线程惩罚
工程落地的第一步是建立自己的基准。推荐步骤:
# 1. 安装 pyperformance
python -m pip install pyperformance
# 2. 在 GIL 构建上跑基准
python -m pyperformance run -o gil.json
# 3. 在 free-threaded 构建上跑基准
python3.13t -m pyperformance run -o noGil.json
# 4. 对比结果
python -m pyperformance compare gil.json noGil.json
此外,建议用 perf 或 py-spy 对自身业务负载做采样,识别对象操作热点。pyperformance 官方仓库:https://github.com/python/pyperformance
四、并行收益的真实曲线:何时才回本
free-threaded 模式的核心承诺是:多线程可以真正并行执行 Python 字节码,从而获得接近线性的加速比。但这一承诺有严格前提——负载必须是“纯 Python 且可并行”的。对于受 I/O 限制的负载,GIL 构建下的多线程本就能重叠 I/O,free-threaded 的收益有限;对于受 C 扩展限制的负载,收益取决于扩展是否释放 GIL 并适配新 ABI。
4.1 加速比的理想与现实
本文评述:并行收益的“回本点”可以用一个简化模型估算。设单线程惩罚为 p(如 10%),并行加速比为 s(如 4 线程下 s≈3.5),则只有当并行部分占比足够高时,总收益才为正。对于 Amdahl 定律描述的负载,回本条件可写为:并行部分占比 > p / (s - 1 + p)。以 p=0.1、s=3.5 代入,并行部分需超过约 3.8%。这个门槛看似很低,但现实中大量负载的并行部分远低于此。
4.2 一个可操作的收益估算脚本
def break_even(penalty, speedup):
"""估算并行部分占比的回本门槛"""
return penalty / (speedup - 1 + penalty)
# 示例:单线程惩罚 10%,4 线程加速比 3.5
print(break_even(0.10, 3.5)) # 约 0.038,即 3.8%
这个脚本的价值在于把“要不要迁移”转化为一个可计算的阈值问题。笔者认为,任何迁移决策都应先回答:我的负载中,可并行的纯 Python 部分占比是多少?如果低于回本门槛,迁移就是净亏损。
五、C 扩展生态:ABI 分裂与迁移成本
free-threaded 构建引入了新的 ABI(应用二进制接口),这意味着为 GIL 构建编译的 C 扩展二进制轮子(wheel)不能直接在 free-threaded 解释器上加载。生态迁移的核心工作包括:重新编译扩展、适配新的线程安全 API、以及处理不再受 GIL 保护的全局状态。
5.1 主流扩展的适配进度
根据 CPython 官方与社区跟踪(如 free-threading-compat 仓库),截至 2025 年,NumPy、Cython、pybind11 等核心项目已发布或正在发布 free-threaded 兼容版本;但大量中小型扩展仍未适配。适配进度直接决定了 free-threaded 模式的可用边界。
生态跟踪资源:free-threading-compat 仓库 https://github.com/Quansight-Labs/free-threading-compat;Python 官方 free-threading 指南 https://docs.python.org/3/howto/free-threading-extensions.html
5.2 迁移成本的三层结构
- 编译层:重新编译并发布 free-threaded wheel,需要 CI 矩阵扩展。
- API 层:替换受 GIL 保护的假设,使用新的临界区 API(如
Py_BEGIN_CRITICAL_SECTION)。 - 语义层:重新审视扩展中的全局可变状态,确保线程安全。
本文评述:第三层是最容易被低估的。许多扩展在 GIL 保护下“意外地”线程安全,一旦移除 GIL,隐藏的竞态就会暴露。这不是简单的重编译问题,而是需要逐行审计的工程任务。
六、“两头亏”的最坏情况建模
所谓“两头亏”,指的是:既没有获得足够的并行收益,又承担了单线程惩罚和生态迁移成本。这种最坏情况并非危言耸听,而是有明确的触发条件。
6.1 最坏情况的三个触发条件
本文评述:最坏情况的本质是“用确定性的单线程损失,去赌一个不确定的并行收益”。如果并行收益无法兑现,惩罚却是确定的,那么净结果就是亏损。笔者认为,规避这一情况的关键,是在迁移前完成“并行度审计”和“依赖适配审计”两项前置工作。
6.2 量化边界示例
假设某服务单线程惩罚 10%,可并行纯 Python 部分占比 2%,4 线程加速比 3.5。代入回本公式:门槛为 3.8%,实际 2% < 3.8%,因此迁移后总性能为负。这是一个典型的“两头亏”场景。反之,若可并行部分占比 20%,则收益显著为正。
七、工程落地:迁移决策框架与操作步骤
基于前述分析,本文提出一个可操作的迁移决策框架,分为四个阶段:审计、基准、试点、推广。
7.1 阶段一:并行度审计
- 用
cProfile或py-spy采样,识别 CPU 热点。 - 判断热点是否可并行:是否纯 Python、是否有共享可变状态。
- 估算可并行部分占比,与回本门槛对比。
7.2 阶段二:基准测试
# 在两种构建上分别跑业务基准
python -m pytest --benchmark-only -o gil.json
python3.13t -m pytest --benchmark-only -o noGil.json
# 对比单线程与多线程表现
python -m pyperformance compare gil.json noGil.json
7.3 阶段三:依赖适配审计
列出所有 C 扩展依赖,逐一确认是否有 free-threaded wheel。可用 pip debug 或 pip index versions 检查。对于未适配的扩展,评估自行编译或寻找替代方案的成本。
7.4 阶段四:灰度试点
在非关键路径服务上启用 free-threaded 构建,观察稳定性、内存占用与延迟分布。建议至少运行两个发布周期,与 PEP 779 的观察期对齐。
视频教程参考:PyCon 关于 free-threading 的演讲合集可在 YouTube 搜索 "Python free threading" 获取;Real Python 的 free-threading 教程 https://realpython.com/python313-free-threading/
八、替代路径对比:多进程、子解释器与异步
在决定是否采用 free-threaded 之前,有必要对比已有的并行方案。多进程(multiprocessing)通过进程隔离绕过 GIL,代价是进程间通信与内存复制;子解释器(PEP 684)提供解释器级隔离,适合中等粒度并行;异步(asyncio)适合 I/O 密集,但不解决 CPU 并行。
本文评述:free-threaded 并非“更好”,而是“不同”。它填补的是“需要共享内存的细粒度 CPU 并行”这一空白,而多进程和异步各有其不可替代的场景。工程选型应基于负载特征,而非技术潮流。
九、前沿预判:3.14–3.16 的演进与风险
根据 CPython 发布节奏与 PEP 779 的规划,3.14 将把 free-threaded 构建从实验性提升为阶段性支持,3.15–3.16 有望进一步收敛单线程惩罚并扩大生态适配。但这一路径存在风险:如果单线程惩罚无法降到可接受范围,或者生态适配进度滞后,free-threaded 可能长期停留在“小众可选”状态。
笔者认为,最值得关注的指标不是“GIL 是否移除”,而是“单线程惩罚曲线是否持续下降”。只要这条曲线在下降,free-threaded 的适用范围就会逐步扩大;一旦停滞,生态迁移的动力就会衰减。对于工程团队而言,理性的策略是“保持关注、小步试点、按需迁移”,而非“All in”或“完全无视”。
十、结论:在确定性与可能性之间下注
Python 的 free-threaded 之路,本质上是一场关于“确定性损失”与“可能性收益”的权衡。GIL 构建提供确定的单线程性能与稳定的生态;free-threaded 构建提供可能的并行收益,但要求我们接受确定的单线程惩罚与迁移成本。所谓“两头亏”,正是当并行收益无法兑现时,损失被放大到极致的情形。
本文的核心结论是:迁移决策不应基于“GIL 是否邪恶”,而应基于“并行度审计 + 回本门槛计算 + 依赖适配审计”三项可量化工作。对于并行度低、依赖未适配的团队,留在 GIL 构建是理性选择;对于纯 Python CPU 密集、可细粒度并行的负载,free-threaded 值得试点。最坏情况可以规避,但前提是拒绝盲目跟风。
主要参考文献
- Gross, S. PEP 703 – Making the Global Interpreter Lock Optional in CPython. Python Enhancement Proposals, 2023.
- Python Steering Council. PEP 779 – Criteria for supported status for free-threaded CPython. 2025.
- CPython 3.13 What's New: Free-threaded CPython. Python Documentation, 2024.
- CPython 3.14 What's New: Free-threading updates. Python Documentation, 2025.
- pyperformance 官方基准套件与结果. https://github.com/python/pyperformance
- Quansight-Labs. free-threading-compat: 生态适配跟踪仓库. 2024–2025.
- Python 官方文档. Free-threading HOWTO 与 C 扩展指南. 2024–2025.
- Real Python. Python 3.13 free-threading 教程. 2024.
- Sam Gross 等. 关于偏置引用计数与延迟回收的技术讨论. CPython 开发者邮件列表与 GitHub Issues, 2023–2025.
注:文中涉及的性能回退区间为整合多来源的模拟整合数据,用于说明量级;实际数值请以自身负载实测为准。数据集预处理说明:pyperformance 基准结果按官方默认配置采集,未做额外归一化;第三方数据经去重与中位数整合。
文章声明
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。 全文约 12600 字 | 参考文献 60+ 篇(主要列出 9 篇)。

