2007年9月,Python创始人Guido van Rossum在一篇博客文章中写下了一段被无数次引用的话:
“我会欢迎移除GIL的补丁——但前提是单线程程序的性能不能下降。”
这个看似简单的条件,在此后的十六年里成为一道不可逾越的高墙。无数开发者尝试翻越,却都折戟而归。直到2023年10月,Python指导委员会才正式接受PEP 703,承诺在未来五年内逐步让GIL成为可选项。
为什么一个看起来如此"明显"的设计缺陷,需要三十年才能解决?
答案藏在三个相互冲突的技术目标之中:单线程性能、多线程并行性、以及C扩展兼容性。这三个目标构成的"不可能三角",让Python社区在三十年间反复权衡、不断妥协。
一个锁的诞生:1992年的务实选择
要理解GIL为何存在,必须回到Python的起源。
Python诞生于1989年,那时的计算机世界与今天截然不同。多核处理器尚未普及,线程编程还是新兴概念。Guido van Rossum在设计Python时面临一个核心问题:如何管理内存?
CPython选择了引用计数(Reference Counting)作为内存管理机制。每个Python对象都有一个引用计数器,记录有多少变量指向它。当计数归零,内存立即释放。这种方式简单高效,但有一个致命缺陷:不是线程安全的。
想象两个线程同时操作同一个对象:
# 线程1: a = obj # 试图增加引用计数
# 线程2: del obj # 试图减少引用计数
如果两个线程同时修改引用计数器,可能产生竞态条件:计数器被错误地增减,导致内存泄漏或更糟的情况——对象被提前释放,程序崩溃。
解决方案有两种:
方案一:细粒度锁——为每个对象加锁。Jython(Python的Java实现)采用了这种方式。但这意味着每个对象都需要额外的锁开销,频繁的加锁解锁操作会严重拖慢性能。
方案二:全局锁——用一把大锁锁住整个解释器。任何时刻只有一个线程可以执行Python字节码。
1992年,Guido选择了方案二。这个决定的背后有几重考量:
单线程性能优先。当时绝大多数程序都是单线程的,GIL的实现极其简单——一次加锁解锁,几乎没有额外开销。相比之下,细粒度锁需要为每个操作加锁,代价高昂。
C扩展生态。Python的一大优势是能轻松调用C库。GIL的存在意味着C扩展开发者不需要操心线程安全问题——在持有GIL时,他们的代码独占解释器,不会有并发冲突。这大大降低了C扩展的开发门槛,催生了NumPy、Pandas等科学计算生态的繁荣。
硬件现实。1990年代的计算机大多是单核CPU,多线程并行本身就没有太大意义。GIL"锁死"多线程的缺陷,在当时根本不是问题。
这个设计在当时是务实的、甚至可以说是明智的。但没有人预料到,多核时代来得如此之快,Python流行得如此之广。
第一次失败:1999年的Greg Stein实验
1999年,Greg Stein(后来成为Apache软件基金会主席)做出了第一次严肃的尝试。他对Python 1.5进行了改造,用细粒度锁替换了GIL。
实验结果令人失望。
根据Guido在2007年文章中的回忆,即使在当时锁操作最快的Windows平台上,移除GIL后单线程性能下降了近一半。这意味着:使用两个CPU核心的无GIL版本,其吞吐量仅略高于使用单个CPU核心的带GIL版本。
这个结果揭示了一个残酷的事实:细粒度锁的开销,比GIL的串行化代价更高。
每次对象访问都需要获取锁、检查锁、释放锁。即使没有竞争,这些操作也会产生大量CPU周期消耗。更糟糕的是,现代CPU的缓存一致性协议(如MESI)会让跨核心的锁操作变得极其昂贵——每次锁争用都可能导致缓存行在不同核心间来回传递。
Greg Stein的实验为Python社区确立了一个不成文的规则:任何GIL移除方案,必须首先回答一个问题——单线程性能代价有多大?
为什么原子操作这么慢?
理解这个问题的关键在于原子操作的开销。
现代CPU提供的原子操作(如CAS——Compare-And-Swap)看似只是一条指令,实际上却暗藏玄机。
在单核时代,原子操作只需要禁用中断就能保证原子性。但在多核处理器上,原子操作需要获取总线锁或缓存行锁,确保所有核心对同一内存位置的访问被串行化。这涉及:
- 缓存锁定——让其他核心的缓存副本失效
- 总线通信——所有核心必须就谁获得锁达成共识
- 内存屏障——防止CPU指令重排序
这些操作需要跨核心协调,代价是普通内存操作的几十甚至上百倍。
Sam Gross在PEP 703中给出了一个惊人的数字:如果简单地将CPython的所有引用计数操作替换为原子版本,pyperformance基准测试的平均性能会下降60%。
这就是为什么Python社区可以容忍GIL存在三十年——因为"显然正确"的解决方案,会让95%以上的单线程Python程序变慢一半以上。
第二次尝试:Larry Hastings的Gilectomy
2016年,PyCon大会上,Larry Hastings展示了他的"Gilectomy"项目。他尝试用一种叫"缓冲引用计数"(Buffered Reference Counting)的技术来解决问题。
核心思路是:不要每次引用计数变化都立即更新,而是将变化记录到日志中,由专门的线程批量处理。
这个想法来自《垃圾回收手册》,理论上可以大幅减少原子操作的开销。但Hastings很快遇到了麻烦。
他在2017年的Python语言峰会上展示了性能数据:在CPU时间上,Gilectomy仍远慢于标准CPython;只有在看墙钟时间(Wall Time)时,多线程版本的Gilectomy才勉强接近单线程CPython的水平。
更致命的是,缓冲引用计数无法提供实时的引用计数。这意味着像weakref这样的功能——依赖实时引用计数来判断对象是否存活——无法正常工作。Hastings承认这可能是Gilectomy的"根本性限制"。
Gilectomy项目的另一个教训是:对象分配器也需要改造。CPython内置的pymalloc不是线程安全的。Hastings尝试添加各种细粒度锁,但性能提升有限。他最终考虑转向TCMalloc或mimalloc这类现代多线程内存分配器。
PyPy的STM实验:另一条路径
在CPython社区为GIL挣扎时,PyPy团队尝试了一条完全不同的路:软件事务内存(Software Transactional Memory,STM)。
PyPy开发者Armin Rigo在2011年提出:与其用锁保护每个操作,不如让每个字节码执行成为一个"事务"。如果检测到冲突,就回滚并重试。
这个想法在学术界有深厚基础——事务内存的概念最早由Tom Knight在1986年提出。Clojure语言已经证明了STM在生产环境中的可行性。
但PyPy的STM实现面临一个根本性挑战:Python语言的语义。Python中大量操作依赖于副作用——读取文件、发送网络请求、修改全局状态。这些操作在事务中执行时,如果事务回滚,副作用如何撤销?
Rigo承认,初始实现的性能可能比标准PyPy慢10倍。即使经过大量优化,也难以达到与带GIL版本持平的水平。
更重要的是,PyPy的STM需要深度修改整个解释器。这使得它难以被CPython社区采纳——后者的生态依赖于大量C扩展,而这些扩展都不是为STM世界设计的。
第三条路:PEP 684的子解释器方案
在PEP 703之外,还有一种思路:不取消GIL,而是让每个子解释器拥有自己的GIL。
Eric Snow提出的PEP 684在2023年4月被接受,成为Python 3.12的一部分。这个方案的核心是:一个进程内可以有多个Python解释器实例,每个实例有独立的GIL。
这听起来像是一个完美的折中:既不需要修改所有C扩展,又能实现真正的并行。但实践中,子解释器之间的通信成本极高——它们无法直接共享Python对象,必须通过序列化或共享内存来交换数据。
更重要的是,大多数现有的C扩展并不支持子解释器。它们可能依赖全局状态,在多解释器环境下会出错。
PEP 684代表了一种"增量改进"的思路:先让架构支持多GIL,再逐步清理全局状态。这为最终的GIL移除铺平了道路,但本身并没有解决单进程内多线程并行的问题。
Sam Gross的突破:为什么PEP 703成功了?
2021年10月,Meta的工程师Sam Gross在Python语言峰会上展示了他的nogil分支。三年后,PEP 703被正式接受。
为什么这次成功了?三个关键技术突破改变了局面。
突破一:偏置引用计数(Biased Reference Counting)
Gross没有简单地将所有引用计数操作原子化,而是采用了Jiho Choi等人在2018年论文中提出的"偏置引用计数"技术。
核心观察是:大多数对象只被创建它的线程访问。因此,可以将引用计数分为两部分:
- 本地计数:由创建对象的线程维护,使用普通(非原子)操作
- 共享计数:由其他线程维护,使用原子操作
当创建线程修改引用计数时,直接操作本地计数,无需原子指令。只有当其他线程访问对象时,才需要原子操作。
这种设计利用了局部性原理:大多数引用计数操作都是线程本地的,几乎零开销;只有少数跨线程访问会产生原子操作成本。
突破二:不朽对象(Immortal Objects)
某些对象永远不会被释放:True、False、None、小整数、内置类型对象等。这些对象被频繁访问,如果每次访问都要修改引用计数,会产生大量无意义的原子操作。
Gross的方案是:将这些对象标记为"不朽"——引用计数字段被设为特殊值,Py_INCREF和Py_DECREF对它们是空操作。
这个优化虽然对单线程性能略有拖累(需要检查对象是否不朽),但在多线程环境下可以避免大量缓存行争用。
突破三:延迟引用计数
函数对象、模块对象、代码对象等生命周期较长,且经常被多线程访问。为它们维护精确的引用计数成本很高。
Gross引入了"延迟引用计数":跳过解释器栈上的引用计数操作,由垃圾回收器在安全点统一处理。
这种权衡是有代价的:这些对象只能由垃圾回收器释放,无法实现精确的内存管理。但对于Python程序中本来就存在循环引用的对象来说,这种代价是可接受的。
配套改进:mimalloc与线程安全容器
除了引用计数,Gross还解决了两个关键基础设施问题:
内存分配器:CPython的pymalloc不是线程安全的。Gross将其替换为Microsoft的mimalloc——一个专为多线程优化的现代内存分配器。
容器线程安全:dict、list、set等内置容器需要内部锁来保护并发修改。这些锁的设计必须在保证正确性的同时,尽量减少对单线程性能的影响。
性能数据:从40%惩罚到10%目标
那么,PEP 703的实际性能如何?
Python 3.13(2024年10月发布)首次包含了实验性的自由线程构建。根据官方文档,单线程性能惩罚约为40%(基于pyperformance基准测试)。这主要是因为PEP 659的"特化自适应解释器"在自由线程模式下被禁用。
Python 3.14(2025年10月发布)大幅改进了自由线程性能。根据Miguel Grinberg的基准测试:
| 测试 | Python 3.13标准 | Python 3.13 FT | Python 3.14标准 | Python 3.14 FT |
|---|---|---|---|---|
| 单线程Fibonacci | 8.26s | 12.40s (慢50%) | 6.59s | 7.05s (慢7%) |
| 4线程Fibonacci | 37.20s | 21.14s (快76%) | 32.60s | 10.80s (快3倍) |
| 单线程Bubble Sort | 2.82s | 4.13s (慢46%) | 2.18s | 2.66s (慢22%) |
| 4线程Bubble Sort | 11.54s | 9.83s (快17%) | 10.55s | 6.23s (快69%) |
数据揭示了一个清晰的趋势:
- 单线程开销正在缩小:从3.13的40-50%,降到3.14的7-22%
- 多线程加速显著:4线程工作负载在3.14上快2-3倍
Python社区的目标是在未来版本中,将单线程性能惩罚降至10%以内。
为什么是现在?时代背景的变化
GIL争议持续三十年,为什么PEP 703能在2023年成功?
多核处理器成为标配
1990年代的计算机大多是单核CPU。但到了2020年代,即使是入门级笔记本也有4-8个核心,服务器级CPU动辄64核以上。GIL的"锁死单线程"问题从理论困扰变成了实际瓶颈。
AI/ML工作负载的压力
机器学习训练和推理是典型的CPU密集型任务。PyTorch核心开发者Zachary DeVito在PEP 703中写道:
“在PyTorch中,我们经常用72个进程来代替一个进程——因为GIL。日志记录、调试、性能调优在这个规模上困难了一个数量级。”
Meta、DeepMind等公司愿意投入工程资源支持GIL移除,正是因为AI工作负载的规模化需求。
Python生态的成熟
三十年的发展让Python拥有了庞大的C扩展生态。这些扩展大多依赖GIL提供的线程安全保证。Gross的设计没有破坏这种兼容性——C扩展可以继续依赖GIL,直到它们准备好支持自由线程。
替代方案的教训
Jython、IronPython、PyPy STM等替代方案提供了宝贵的经验教训。Gross的设计吸收了这些经验:避免Jython的细粒度锁开销,借鉴PyPy的延迟引用计数思路,同时保持与现有C扩展的兼容性。
未来五年:渐进式过渡
Python指导委员会采纳PEP 703时,特别强调"渐进式过渡"和"可回滚":
- Python 3.13(2024):实验性自由线程构建,主要供核心开发者和早期采用者测试
- Python 3.14(2025):自由线程成为受支持的选项,但非默认
- 未来版本:根据社区采用情况,可能将自由线程设为默认
- 长期目标:最终完全移除GIL
这个时间表避免了Python 2到3的"断层式升级"灾难。现有的Python代码和C扩展可以继续在GIL模式下运行,无需任何修改。
权衡的艺术:没有完美的方案
回顾GIL的三十年历史,一个清晰的图景浮现出来:这不是一个"技术失误"被"技术进步"纠正的故事,而是一个关于权衡的故事。
单线程性能 vs 多线程并行——这是最核心的矛盾。三十年前的选择是单线程优先,这在当时是正确的。三十年后的选择是两者兼顾,这得益于硬件和软件技术的进步。
简单性 vs 完美性——GIL的实现极其简单,但它限制了多核利用。细粒度锁理论上更完美,但实现复杂且开销巨大。偏置引用计数找到了中间地带:利用局部性原理,在不完美中寻找效率。
兼容性 vs 进步——Python的成功很大程度上归功于其C扩展生态。任何破坏这一生态的方案都是不可接受的。PEP 703的设计花了大量精力确保现有代码"开箱即用"。
正如Guido van Rossum在讨论中所说:
“如果有什么是Python 2到3过渡教给我们的,那就是我们本应该让Python 2和3代码能够在同一个解释器中共存。我们搞砸了那一次,让我们倒退了大约十年。这次不要再搞砸了。”
结语:技术债务的优雅清偿
GIL不是bug,它是技术债——一个在当时正确、但随着时间推移变得不再最优的设计决策。
清偿这笔债务用了三十年。这漫长的过程不是因为开发者懒惰或无能,而是因为解决方案需要满足一个几乎不可能的条件:
在不牺牲单线程性能的前提下,实现真正的多线程并行。
Sam Gross通过偏置引用计数、不朽对象、延迟引用计数三项技术创新,终于跨过了这道门槛。
Python 3.14的自由线程构建标志着这门语言的成年礼:它终于准备好面对多核时代的挑战,同时没有背叛过去三十年建立起来的生态。
这不是一个英雄故事。这是一个关于工程务实主义的故事——知道何时妥协,何时坚持,以及如何在不完美的约束下找到最优解。
参考资料
- PEP 703 – Making the Global Interpreter Lock Optional in CPython, Sam Gross, 2023
- “It isn’t Easy to Remove the GIL”, Guido van Rossum, 2007
- “GIL removal and the Faster CPython project”, LWN.net, 2023
- “A viable solution for Python concurrency”, LWN.net, 2021
- “Progress on the Gilectomy”, LWN.net, 2017
- “We need Software Transactional Memory”, PyPy Blog, Armin Rigo, 2011
- PEP 659 – Specializing Adaptive Interpreter, Mark Shannon, 2021
- PEP 684 – A Per-Interpreter GIL, Eric Snow, 2022
- “Python 3.14 Is Here. How Fast Is It?”, Miguel Grinberg, 2025
- “What Is the Python Global Interpreter Lock (GIL)?”, Real Python
- Biased Reference Counting, Jiho Choi et al., 2018
- Python Documentation: Free-Threading Support, Python 3.14