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)看似只是一条指令,实际上却暗藏玄机。

在单核时代,原子操作只需要禁用中断就能保证原子性。但在多核处理器上,原子操作需要获取总线锁或缓存行锁,确保所有核心对同一内存位置的访问被串行化。这涉及:

  1. 缓存锁定——让其他核心的缓存副本失效
  2. 总线通信——所有核心必须就谁获得锁达成共识
  3. 内存屏障——防止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)

某些对象永远不会被释放:TrueFalseNone、小整数、内置类型对象等。这些对象被频繁访问,如果每次访问都要修改引用计数,会产生大量无意义的原子操作。

Gross的方案是:将这些对象标记为"不朽"——引用计数字段被设为特殊值,Py_INCREFPy_DECREF对它们是空操作。

这个优化虽然对单线程性能略有拖累(需要检查对象是否不朽),但在多线程环境下可以避免大量缓存行争用。

突破三:延迟引用计数

函数对象、模块对象、代码对象等生命周期较长,且经常被多线程访问。为它们维护精确的引用计数成本很高。

Gross引入了"延迟引用计数":跳过解释器栈上的引用计数操作,由垃圾回收器在安全点统一处理。

这种权衡是有代价的:这些对象只能由垃圾回收器释放,无法实现精确的内存管理。但对于Python程序中本来就存在循环引用的对象来说,这种代价是可接受的。

配套改进:mimalloc与线程安全容器

除了引用计数,Gross还解决了两个关键基础设施问题:

内存分配器:CPython的pymalloc不是线程安全的。Gross将其替换为Microsoft的mimalloc——一个专为多线程优化的现代内存分配器。

容器线程安全dictlistset等内置容器需要内部锁来保护并发修改。这些锁的设计必须在保证正确性的同时,尽量减少对单线程性能的影响。

性能数据:从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%)

数据揭示了一个清晰的趋势:

  1. 单线程开销正在缩小:从3.13的40-50%,降到3.14的7-22%
  2. 多线程加速显著: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时,特别强调"渐进式过渡"和"可回滚":

  1. Python 3.13(2024):实验性自由线程构建,主要供核心开发者和早期采用者测试
  2. Python 3.14(2025):自由线程成为受支持的选项,但非默认
  3. 未来版本:根据社区采用情况,可能将自由线程设为默认
  4. 长期目标:最终完全移除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的自由线程构建标志着这门语言的成年礼:它终于准备好面对多核时代的挑战,同时没有背叛过去三十年建立起来的生态。

这不是一个英雄故事。这是一个关于工程务实主义的故事——知道何时妥协,何时坚持,以及如何在不完美的约束下找到最优解。


参考资料

  1. PEP 703 – Making the Global Interpreter Lock Optional in CPython, Sam Gross, 2023
  2. “It isn’t Easy to Remove the GIL”, Guido van Rossum, 2007
  3. “GIL removal and the Faster CPython project”, LWN.net, 2023
  4. “A viable solution for Python concurrency”, LWN.net, 2021
  5. “Progress on the Gilectomy”, LWN.net, 2017
  6. “We need Software Transactional Memory”, PyPy Blog, Armin Rigo, 2011
  7. PEP 659 – Specializing Adaptive Interpreter, Mark Shannon, 2021
  8. PEP 684 – A Per-Interpreter GIL, Eric Snow, 2022
  9. “Python 3.14 Is Here. How Fast Is It?”, Miguel Grinberg, 2025
  10. “What Is the Python Global Interpreter Lock (GIL)?”, Real Python
  11. Biased Reference Counting, Jiho Choi et al., 2018
  12. Python Documentation: Free-Threading Support, Python 3.14