500 个 Python 解释器

随着 Python 3.13 最终发布日期的临近,关于 3.13 引入可选全局解释器锁(GIL)的讨论有所增加。实际上有两个并行的努力来提高 Python 的多线程性能,一是可选 GIL(在PEP 703中指定),二是每个解释器 GIL 的引入(在PEP 684中指定并在Python 3.12中引入)。

在本文中,作者解释了 GIL 的一些历史、它如何影响 Python 的 C API 设计,以及在 C API 级别采取了哪些步骤才达到今天的状况。还简要讨论了[PEP 684]和[PEP 703]如何协同工作。

个人与 GIL 的经历

  • 2005 年夏天在斯坦福大学参加为期数周的游戏开发夏令营,首次接触 Python 2.4,当时 PyGame 约为 1.7.1 版本,使用 livewires 库简化了 PyGame 背后的很多逻辑,作者通过该库加载大量精灵,因加载方式导致加载时间极长,经他人建议使用threading模块后,加载时间反而增加,后得知是 GIL 导致。
  • 《文明 IV》使用 Python 进行游戏逻辑和脚本系统,地图上单位越多、执行回合越多,游戏运行越慢,《文明 V》团队切换到 Lua 后性能大幅提升。

GIL 的相关信息

  • 线程引入历史:1998 年 2 月 17 日 Python 1.5 版本引入线程,同时引入PyInterpreterStatePyThreadState对象及 GIL,之前 Python 只有一个简单的评估执行步骤code_evalmultiprocessing模块在历史上比threading快,因为它创建新的解释器实例,无需担心 GIL,但有子进程和通信成本。
  • GIL 存在的原因:1998 年 Python 处于早期阶段,缺乏社区方向,CPython 实现借鉴了 Tcl 和 OpenGL 的设计,引入 GIL 是为了让 Python 更易于嵌入,在 3.5 版本之前,模块导入后无法真正隔离,GIL 用于分离原生代码和实际绑定。
  • C API 级的 GIL 工作原理:用户创建PyThreadState(即子解释器),通过Py_NewInterpreter调用,将其添加到PyInterpreterState跟踪的列表中,获取和释放 GIL 以执行 Python 代码,多个PyThreadState不能同时执行 Python 字节码,但可以同时执行多个 C 调用,扩展和嵌入开发人员应在执行长时间纯 C 操作时暂时释放 GIL。Python 3.12 引入了允许创建与全局解释器无关的子解释器的 APIPy_NewInterpreterFromConfig

示例及问题

  • 示例创建 500 个解释器状态,先创建隔离配置,然后创建线程并初始化解释器状态,执行 Python 代码并打印,最后关闭解释器。但在调试模式下创建和销毁 500 个 Python 解释器状态会出现内存 corruption、synchronization issues 等问题,Python 的内存调试断言会触发,作者已在 Python GitHub 仓库提交问题。

尽管目前 Python 空间的状况不佳,但作者认为随着时间的推移,GIL 最终会消失,未来会有更好的解决方案。

阅读 14
0 条评论