Python 中有一把著名的锁——全局解释器锁(Global Interpreter Lock,简称 GIL)。它的作用是防止多个本地线程同时执行 Python 字节码。在 CPython 解释器中,GIL 的存在导致 Python 无法实现真正的多线程并行执行,尽管操作系统层面的线程是并发的。
这把锁在 Python 早期发展中具有积极的作用,特别是在单核 CPU 时代,它简化了内存管理。然而,随着多核 CPU 的普及,GIL 阻碍了 Python 在多核环境下的并行编程能力,引起了开发者们的广泛诟病。GIL 影响的主要是 CPU 密集型任务,例如科学计算、数值计算以及人工智能模型训练等场景。
在最近发布的官方文档或社区讨论中,概括了 GIL 对科学计算(主要是 AI/ML)造成的四类主要问题:
- 并行化表达困难:GIL 导致许多并行化操作难以高效表达,影响了强化学习、DeepMind 相关研究、医学治疗及生物信息学等领域。
- 库可用性受限:GIL 影响了 Python 核心科学计算库的可用性,例如 PyTorch、scikit-learn、NumPy 等在多线程场景下性能受限。
- GPU 资源利用率低:GIL 导致无法充分利用 GPU 资源,特别是在计算机视觉任务中,CPU 和 GPU 之间的数据交换和任务调度受到限制。
- 模型部署困难:GIL 导致难以部署基于神经网络的 AI 模型,尤其是在高并发服务场景下。
社区中想要移除 GIL 的呼声以及尝试一直此起彼伏,但这个话题因涉及底层架构变更而一直悬而未决。从一个积重已久的庞大项目中移除一个根基性的设计谈何容易?
PEP-703:使 GIL 成为可选项
2023 年初,这个话题再次热了起来。PEP-703 于 1 月 9 日提出,虽然目前仍是'草案'状态未被正式采纳,但其意义十分重大。这份 PEP 的作者是 Sam Gross,他是 nogil 项目的作者。
PEP 的标题是《使 CPython 的 GIL 成为可选项》(Making the Global Interpreter Lock Optional in CPython),内容详实,正文超过 1 万字。简单而言,这份提案提议给 CPython 增加一个构建时配置项 --disable-gil,作用是构建出一个线程安全的无 GIL 的解释器。
为了实现无 GIL 的解释器,Python 底层的部分设计必须作出变更,主要包括以下四类:
- 引用计数:需要改进引用计数的实现,使其在无 GIL 环境下保持线程安全。
- 内存管理:调整内存分配和管理机制,避免竞争条件。
- 容器线程安全:确保标准库中的容器(如列表、字典)在无锁情况下也能安全使用。
- 锁和原子 API:引入新的锁机制和原子操作 API 来替代原有的 GIL 保护。
如果这份 PEP 被采纳实现的话,它会带来一个不容忽视的问题:Python 将发布两个不同版本的解释器,而第三方库也要相应地开发、维护和发布两个版本的软件包。这可能导致生态割裂。
PEP-703 的作者也考虑到了这个问题,他提出的解决方案是与 Anaconda 合作发布无 GIL 的 Python,同时在 conda 里集中发布管理那些兼容了新 Python 的库。考虑到 Anaconda 在科学计算与数值计算领域的强大影响力,此举既能较好地发挥 nogil Python 的用处,又能减少用户及三方库开发者面对两种发行版时的割裂感。
性能挑战与权衡
值得注意的是,nogil 的 Python 还有一个更大的问题,那就是会影响单线程程序的性能。基于 Python 3.11 版本,实现了有偏见的引用计数及永生对象后,Python 单线程性能会变慢约 10%。
尽管这个数值在最新的 nogil 原型版本上可以降低到 5%,但是,另外至少还有两项难以规避的性能下降点:
- 2%:来自全局的自由列表(主要是元组和浮点数自由列表)。
- 1.5%:来自集合中每个对象的互斥锁(字典、列表、队列)。
单线程的代码才是最广泛的使用场景,可以说这会影响到每一个 Python 用户。任何试图移除 GIL 的项目都不可避免要面临这项挑战。
与其他方案的对比
尽管存在着以上的两大问题,但 PEP-703 还是很有可取之处的。
相比于 2015 年提出的著名的 Gilectomy 项目(由 GIL 和 ectomy 组合而成,ectomy 是一个医学上的术语'切除术'),nogil 在单线程的性能上要快得多,同时可扩展性也更好。
相比于 2021 年火热的'香农计划'的作者 Eric Snow 提出的 PEP-684 方案(给每个子解释器创建 GIL),后者一方面需要实现作为前提的多个 PEP(如 PEP-554、PEP-683),另一方面需要用户处理多子解释器间共享变量的麻烦。在香农计划的规划中,PEP-554 与 PEP-684 已经囊括在内了,版本目标是充分利用 Python 的子解释器,让子解释器使用各自的 GIL,从而实现多线程的并行。


