Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 684 – 针对每个解释器的 GIL

作者:
Eric Snow <ericsnowcurrently at gmail.com>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
要求:
683
创建日期:
2022年3月8日
Python 版本:
3.12
发布历史:
2022年3月8日, 2022年9月29日, 2022年10月28日
决议:
Discourse 消息

目录

摘要

自 Python 1.5 (1997) 以来,CPython 用户可以在同一进程中运行多个解释器。然而,同一进程中的解释器始终共享大量全局状态。这是 bug 的来源,并且随着越来越多的人使用该功能,其影响也越来越大。此外,充分的隔离将促进真正的多核并行,届时解释器不再共享 GIL。本提案中概述的更改将实现这种程度的解释器隔离。

高层总结

从高层次来看,本提案以以下方式更改 CPython:

  • 在充分隔离的情况下,停止在解释器之间共享 GIL
  • 为隔离设置添加多个新的解释器配置选项
  • 防止不兼容的扩展造成问题

GIL

GIL 保护对 CPython 大多数运行时状态的并发访问。因此,所有这些受 GIL 保护的全局状态都必须在 GIL 之前移到每个解释器中。

(在少数情况下,可以使用其他机制来确保线程安全共享,例如锁或“不朽”对象。)

CPython 运行时状态

正确隔离解释器需要将 CPython 的大部分运行时状态存储在 PyInterpreterState 结构中。目前,只有一部分存储在那里;其余的要么在 C 全局变量中,要么在 _PyRuntimeState 中。大部分都必须移动。

这与正在进行的(多年来的)努力直接相关,旨在大幅减少全局变量的内部使用,并将运行时状态整合到 _PyRuntimeStatePyInterpreterState 中。(参见下文的整合运行时全局状态。)该项目本身就具有重要的价值,并且几乎没有争议。因此,虽然每个解释器的 GIL 依赖于该工作的完成,但该项目不应被视为本提案的一部分——而仅仅是一个依赖。

其他隔离注意事项

CPython 的解释器必须严格相互隔离,只有少数例外。在很大程度上,它们已经如此。每个解释器都有自己的所有模块、类、函数和变量的副本。CPython C-API 文档进一步解释

然而,除了已经提到的(例如 GIL),解释器仍然以两种方式共享一些状态。

首先,一些进程全局资源(例如内存、文件描述符、环境变量)是共享的。目前没有计划改变这一点。

其次,由于错误或未考虑多解释器实现的缺陷,某些隔离存在问题。这包括 CPython 的运行时和标准库,以及依赖全局变量的扩展模块。在这种情况下应提交 bug 报告,有些已经提交。

依赖于不朽对象

PEP 683 引入了不朽对象作为 CPython 的内部特性。通过不朽对象,我们可以共享所有解释器之间所有其他不可变的全局对象。因此,本 PEP 无需解决如何处理 公共 C-API 中公开的各种对象的问题。它还简化了处理内置静态类型的问题。(参见下面的全局对象。)

这两个问题都有替代解决方案,但使用不朽对象会使一切变得更简单。如果 PEP 683 未被接受,那么本提案将更新为替代方案。这使我们能够减少本提案中的冗余信息。

动机

我们在此解决的根本问题是 CPython 运行时中缺乏真正的多核并行性(对于 Python 代码)。GIL 是其原因。虽然在实践中通常不是问题,但至少它使 Python 的多核故事变得模糊不清,这使得 GIL 始终令人分心。

隔离的解释器也是支持某些并发模型的有效机制。PEP 554 更详细地讨论了这一点。

间接好处

实现每个解释器 GIL 所需的大部分工作都具有使其无论如何都值得做的好处:

  • 使多解释器行为更可靠
  • 导致解决了长期存在的运行时 bug,这些 bug 以前未被优先处理
  • 暴露(并启发修复)了以前未知的运行时 bug
  • 推动了更清晰的运行时初始化(PEP 432, PEP 587
  • 推动了更清晰、更完整的运行时终结
  • 导致了 C-API 的结构分层(例如 Include/internal
  • 另请参见下面的整合的好处

此外,其中大部分工作都惠及其他 CPython 相关项目:

多解释器的现有用法

多年来,多解释器的 C-API 一直在使用。然而,直到相对较近,该功能才广为人知,也未被广泛使用(mod_wsgi 除外)。

在过去几年中,多解释器的使用量有所增加。以下是一些目前使用该功能的公共项目:

请注意,有了 PEP 554,多解释器的使用量可能会显著增长(通过 Python 代码而不是 C-API)。

PEP 554(标准库中的多解释器)

PEP 554 严格来说是提供一个最小化的标准库模块,让用户可以从 Python 代码访问多个解释器。事实上,它特意避免提出任何与 GIL 相关的更改。但是,请考虑该模块的用户将受益于每个解释器的 GIL,这使得 PEP 554 更具吸引力。

基本原理

在 2014 年的初步调查中,探索了各种可能的 Python 多核解决方案,但每个方案都有其缺点,没有简单的解决方案:

  • 在扩展模块中释放 GIL 的现有做法
    • 对 Python 代码没有帮助
  • 其他 Python 实现(例如 Jython、IronPython)
    • CPython 主导社区
  • 移除 GIL(例如 gilectomy,“no-gil”)
    • 技术风险太大(当时)
  • Trent Nelson 的 “PyParallel” 项目
    • 不完整;当时仅限 Windows
  • 多进程
    • 使其足够有效的工作量太大;在某些情况下(大规模、Windows)惩罚很高
  • 其他并行工具(例如 dask、ray、MPI)
    • 不适合运行时/标准库
  • 放弃多核(例如异步、无所作为)
    • 这只会以悲剧收场

即使在 2014 年,使用隔离解释器的解决方案技术风险不高,而且大部分工作无论如何都值得做,这一点已经相当清楚。(缺点是要完成的工作量很大。)

规范

正如上面总结的,本提案涉及以下更改,按其必须发生的顺序排列:

  1. 将全局运行时状态(包括对象)整合到 _PyRuntimeState
  2. 将几乎所有状态下移到 PyInterpreterState
  3. 最后,将 GIL 下移到 PyInterpreterState
  4. 其他一切
    • 更新 C-API
    • 实施扩展模块限制
    • 与流行的扩展维护者合作,帮助支持多解释器

每个解释器的状态

以下运行时状态将移至 PyInterpreterState

  • 所有不安全共享(完全不可变)的全局对象
  • GIL
  • 当前受 GIL 保护的大多数可变数据
  • 当前受其他每个解释器锁保护的可变数据
  • 可在不同解释器中独立使用的可变数据(也适用于扩展模块,包括多阶段初始化模块)
  • 所有其他未在下面排除的可变数据

此外,部分完整的全局状态已移至解释器,包括 GC、警告和 atexit 钩子。

以下运行时状态不会被移动

  • 安全共享的全局对象(如果有)
  • 不可变数据,通常是 const
  • 实际不可变数据(被视为不可变),例如
    • 某些状态在早期初始化后不再修改
    • 字符串的哈希值 (PyUnicodeObject) 在首次需要时幂等计算,然后缓存
  • 所有保证仅在主线程中修改的数据,包括
    • 仅在 CPython 的 main() 中使用的状态
    • REPL 的状态
    • 仅在运行时初始化期间修改的数据(之后实际不可变)
  • 受某些全局锁(GIL 除外)保护的可变数据
  • 原子变量中的全局状态
  • 可合理地更改为原子变量的可变全局状态

内存分配器

这是隔离解释器工作中最敏感的部分之一。最简单的解决方案是将内部“小块”分配器的全局状态移至 PyInterpreterState,就像我们处理几乎所有其他运行时状态一样。以下将详细阐述其细节和原理。

CPython 提供了一个内存管理 C-API,具有三个分配器域:“raw”、“mem”和“object”。每个域都提供等效于 malloc()calloc()realloc()free() 的功能。可以在运行时初始化期间设置每个域的自定义分配器,并且可以使用相同的 API 包装当前分配器以添加钩子(例如,标准库的 tracemalloc 模块)。分配器目前是运行时全局的,由所有解释器共享。

“raw”分配器预期是线程安全的,默认使用 glibc 的分配器(malloc() 等)。然而,“mem”和“object”分配器预期不是线程安全的,目前可能依赖 GIL 来保证线程安全。这部分是因为两者的默认分配器,即“pyobject”,不是线程安全的。这是因为该分配器的所有状态都存储在 C 全局变量中。(参见 Objects/obmalloc.c。)

因此,我们又回到了隔离运行时状态的问题。为了让解释器停止共享 GIL,必须解决分配器的线程安全问题。如果解释器继续共享分配器,那么我们需要其他方法来获得线程安全。否则,解释器必须停止共享分配器。在这两种情况下,都有许多可能的解决方案,每种方案都有潜在的缺点。

为了保持分配器共享,最简单的解决方案是在 PyMem_Malloc()PyObject_Malloc() 等函数中对“mem”和“object”分配器调用使用细粒度的运行时全局锁。这将影响性能,但有一些方法可以缓解(例如,只有在创建第一个子解释器后才开始加锁)。

另一种保持分配器共享的方法是要求“mem”和“object”分配器是线程安全的。这意味着我们必须使 pyobject 分配器的实现线程安全。这甚至可能涉及使用可扩展分配器(如 mimalloc)重新实现它。潜在的缺点是重新实现分配器的成本以及此类工作固有的缺陷风险。

无论如何,转向要求线程安全的分配器将影响所有嵌入 CPython 且当前设置了非线程安全分配器的人。我们需要考虑谁可能受到影响以及如何减少任何负面影响(例如,添加一个基本的 C-API 来帮助使分配器线程安全)。

如果我们确实停止了在解释器之间共享分配器,那么我们必须只对“mem”和“object”分配器这样做。我们可能还需要保留一组完整的全局分配器用于某些运行时级别的使用。由于查找当前解释器然后通过指针间接获取分配器,这将带来一些性能损失。嵌入者也可能需要为每个解释器提供一个新的分配器上下文。从好的方面看,分配器钩子(例如 tracemalloc)不会受到影响。

最终,我们将选择最简单的方案:

  • 将分配器保留在全局运行时状态中
  • 要求它们是线程安全的
  • 将默认对象分配器(又称“小块”分配器)的状态移至 PyInterpreterState

我们尝试了一个粗略的实现,发现它相当简单,而且性能损失基本为零。

C-API

在内部,解释器状态现在将跟踪导入系统应如何处理不支持与多个解释器一起使用的扩展模块。参见下面的限制扩展模块。我们在此将其称为“PyInterpreterState.strict_extension_compat”。

以下 API 将公开,如果尚未公开的话:

  • PyInterpreterConfig(结构体)
  • PyInterpreterConfig_INIT(宏)
  • PyInterpreterConfig_LEGACY_INIT(宏)
  • PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)

我们将在 PyInterpreterConfig 中添加两个新字段

  • int own_gil
  • int strict_extensions_compat

随着时间的推移,我们可能会根据需要添加其他字段(例如“own_initial_thread”)。

关于初始化宏,PyInterpreterConfig_INIT 将用于获取一个隔离的解释器,该解释器同时避免了对子解释器不友好的功能。它将是PEP 554 中通过 Python 代码创建解释器的默认设置。不受限制的(现状)将继续通过 PyInterpreterConfig_LEGACY_INIT 提供,该宏已用于主解释器和 Py_NewInterpreter()。这不会改变。

关于“主”解释器的一点说明

下文我们多次提到“主”解释器。这指的是在运行时初始化期间创建的解释器,其初始 PyThreadState 对应于进程的主线程。它具有许多独特的职责(例如处理信号),并在运行时初始化/终结期间扮演着特殊角色。它通常(目前)也是唯一的解释器。(另请参见 https://docs.pythonlang.cn/3/c-api/init.html#sub-interpreter-support。)

PyInterpreterConfig.own_gil

如果为 true (1),则新解释器将拥有自己的“全局”解释器锁。这意味着新解释器可以在不被其他解释器中断的情况下运行。这有效地解除了多核的全部使用障碍。这是本 PEP 的根本目标。

如果为 false (0),则新解释器将使用主解释器的锁。这是 CPython 中遗留的(3.12 之前的)行为,所有解释器共享一个 GIL。当使用仍然依赖 GIL 进行线程安全的扩展模块时,这样共享 GIL 可能是可取的。

PyInterpreterConfig_INIT 中,这会是 true。在 PyInterpreterConfig_LEGACY_INIT 中,这会是 false

另外,为了安全起见,目前我们不允许在运行时初始化期间设置了自定义分配器的情况下将 own_gil 设置为 true。包装分配器(例如 tracemalloc)仍然可以正常工作。

PyInterpreterConfig.strict_extensions_compat

PyInterpreterConfig.strict_extension_compat 基本上是用于“PyInterpreterState.strict_extension_compat”的初始值。

限制扩展模块

当状态存储在全局变量中时,扩展模块与运行时存在许多相同的问题。PEP 630 涵盖了扩展必须做些什么才能支持隔离,从而安全地在多个解释器中同时运行的所有细节。这包括处理它们的全局变量。

如果一个扩展实现了多阶段初始化(参见 PEP 489),则它被认为是与多个解释器兼容的。所有其他扩展都被认为是不兼容的。(有关每个解释器 GIL 如何影响该分类的更多详细信息,请参见扩展模块线程安全。)

如果导入了不兼容的扩展,并且当前“PyInterpreterState.strict_extension_compat”值为 true,则导入系统将引发 ImportError。(对于 false,它只是不检查。)这将通过 importlib._bootstrap_external.ExtensionFileLoader 完成(实际上,通过 _imp.create_dynamic()_PyImport_LoadDynamicModuleWithSpec()PyModule_FromDefAndSpec2())。

在主解释器中(或通过 Py_NewInterpreter() 创建的解释器中),此类导入永远不会失败,因为在这两种情况下,“PyInterpreterState.strict_extension_compat”都初始化为 false。因此,保留了遗留的(3.12 之前的)行为。

我们将与流行的扩展合作,帮助它们支持在多个解释器中使用。这可能涉及添加到 CPython 的公共 C-API,我们将根据具体情况进行处理。

扩展模块兼容性

正如扩展模块中指出的,许多扩展在多个解释器中(以及在每个解释器 GIL 下)都能正常工作,而无需进行任何更改。如果此类模块未明确指示支持,导入系统仍将失败。最初,许多扩展模块都不会这样做,因此这可能是令人沮丧的来源。

我们将通过添加一个上下文管理器来解决这个问题,以暂时禁用对多解释器支持的检查:importlib.util.allow_all_extensions()。或多或少,它将修改当前“PyInterpreterState.strict_extension_compat”的值(例如,通过一个私有的 sys 函数)。

扩展模块线程安全

如果一个模块支持与多个解释器一起使用,那主要意味着即使这些解释器不共享 GIL,它也能正常工作。唯一的例外是当模块链接到一个内部全局状态非线程安全的库时。(即使是像静态局部变量作为临时缓冲区这样无害的东西也可能是一个问题。)有了共享的 GIL,该状态受到保护。如果没有,此类模块必须使用锁包装对该状态的任何使用(例如通过调用)。

目前尚不清楚支持多解释器是否与支持每个解释器的 GIL 充分等效,以至于我们可以避免任何特殊安排。这仍然是一个有意义的讨论和调查点。两者之间的实际区别(在 Python 社区中,例如 PyPI)尚未完全理解,无法解决此问题。同样,尚不清楚我们能做些什么来帮助扩展维护者缓解问题(假设这是一个问题)。

与此同时,我们必须假设这种差异足以给现有足够多的扩展模块带来问题。我们将采用的解决方案是:

  • 添加一个 PyModuleDef 槽位,指示扩展可以在每个解释器 GIL 下导入(即选择加入)
  • 将该槽位作为“兼容”扩展定义的一部分,如前所述

缺点是,如果没有模块维护者额外付出努力,即使是微小的努力,任何扩展模块都无法利用每个解释器 GIL。这使得扩展模块兼容性中描述的问题更加复杂,并且适用相同的解决方法。理想情况下,我们会确定差异不足以构成问题。

如果最终我们确实要求在每个解释器 GIL 下导入时选择加入,并且稍后确定没有必要,那么我们可以届时切换默认设置,使旧的选择加入槽位成为空操作,并添加一个新的 PyModuleDef 槽位用于明确选择 *退出*。事实上,从一开始就添加该选择退出槽位是很有意义的。

文档

  • C-API:Doc/c-api/init.rst 的“子解释器支持”部分将详细说明更新后的 API
  • C-API:该部分将解释每个解释器 GIL 的后果
  • importlib:ExtensionFileLoader 条目将注明导入可能在子解释器中失败
  • importlib:将有一个关于 importlib.util.allow_all_extensions() 的新条目

影响

向后兼容性

本提案无意改变任何行为或 API,但有两个例外:

  • 某些扩展将在某些子解释器中导入失败(参见下一节
  • 当前非线程安全的“mem”和“object”分配器现在在与多个解释器结合使用时可能容易受到数据竞争的影响

现有的用于管理解释器的 C-API 将保留其当前行为,新行为将通过新的 API 公开。不打算更改任何其他 API 或运行时行为,包括与稳定 ABI 的兼容性。

有关相关讨论,请参阅下面的C-API 中公开的对象

扩展模块

目前,Python 最常见的用法是主解释器单独运行。本提案对这种场景下的扩展模块没有任何影响。同样,无论好坏,使用现有 Py_NewInterpreter() 创建的多个解释器下的行为没有改变。

请记住,一些扩展在多个解释器中使用时已经出现问题,因为它们将模块状态保存在全局变量中(或由于链接库的内部状态)。它们可能会崩溃,甚至更糟,出现不一致的行为。这正是PEP 630 等提案的动机之一,因此这不是一个新的情况,也不是本提案的后果。

相反,当使用提议的 API 并使用适当设置创建多个解释器时,不兼容扩展的行为将发生变化。在这种情况下,导入此类扩展将失败(在主解释器之外),如限制扩展模块中所述。对于已经在多个解释器中崩溃的扩展,这将是一个改进。

此外,一些扩展模块链接到具有非线程安全内部全局状态的库。(参见扩展模块线程安全。)此类模块将不得不开始用锁包装对该状态的任何直接或间接使用。这是与也实现多阶段初始化并因此指示支持多解释器(即隔离)的其他模块的关键区别。

现在我们来谈谈上面提到的兼容性问题。有些扩展在多个解释器下(以及在每个解释器的 GIL 下)是安全的,尽管它们没有表明这一点。不幸的是,导入系统无法可靠地推断出此类扩展是安全的,因此导入它们仍将失败。这种情况在上面的扩展模块兼容性中有所解决。

扩展模块维护者

一个相关的考虑是,每个解释器的 GIL 可能会推动多解释器使用量的增加,特别是如果 PEP 554 被接受。一些大型扩展模块的维护者对因多解释器使用量增加而带来的负担表示担忧。

具体来说,为某些扩展模块(尽管可能不多)启用多解释器支持将需要大量工作。为了添加该支持,此类模块的维护者(通常是志愿者)将不得不搁置他们的正常优先级和兴趣,专注于兼容性(参见 PEP 630)。

当然,扩展维护者可以自由选择不添加对多解释器使用的支持。但是,用户将越来越多地要求这种支持,特别是如果该功能越来越受欢迎。

无论如何,这种情况对于此类扩展的维护者来说可能会感到压力,特别是当他们在业余时间完成工作时。他们表达的担忧是可以理解的,我们将在限制扩展模块扩展模块兼容性部分讨论部分解决方案。

备选 Python 实现

其他 Python 实现不要求在同一进程中提供多解释器支持(尽管有些已经提供)。

安全隐患

本提案对安全性没有已知影响。

可维护性

一方面,本提案已经推动了多项改进,使 CPython *更*易于维护。预计这种情况将持续下去。另一方面,基础工作已经暴露了运行时中各种预先存在的缺陷,这些缺陷必须得到修复。随着多解释器使用量的增加,预计这种情况也将持续下去。否则,对可维护性不应有显著影响,因此净效应应是积极的。

性能

整合全局变量的工作已经为 CPython 的性能带来了多项改进,既提高了速度又减少了内存使用,预计这将继续下去。具体而言,每个解释器 GIL 的性能优势尚未探索。至少,预计它不会使 CPython 变慢(只要解释器充分隔离)。而且,显然,它将使 Python 代码中的各种多核并行成为可能。

如何教授此内容

PEP 554 不同,这是一项针对少数 C-API 用户的高级功能。不期望教授 API 的具体细节或其直接应用。

也就是说,如果确实要教授,则归结为以下几点:

除了 Py_NewInterpreter(),您还可以使用 Py_NewInterpreterFromConfig() 来创建解释器。您传递的配置指示了您希望该解释器如何行为。

此外,创建隔离解释器的任何扩展模块的维护者可能需要向其用户解释每个解释器 GIL 的后果。首先要解释的是 PEP 554 所教授的隔离解释器所支持的并发模型。这引出了一个观点:使用该并发模型编写的 Python 软件可以利用多核并行,而这目前被 GIL 阻止。

参考实现

<待定>

未解决的问题

  • 我们是否可以要求“mem”和“object”分配器是线程安全的?
  • 每个解释器的 tracemalloc 模块将如何与全局分配器相关联?
  • faulthandler 模块会局限于主解释器(像信号模块一样)吗?还是我们会让全局状态在解释器之间泄漏(受细粒度锁保护)?
  • 根据“整合运行时全局状态”部分拆分出一个包含所有相关信息的说明性 PEP 吗?
  • 一个模块在多个解释器下(隔离)可以工作,但在每个解释器 GIL 下却不能工作的可能性有多大?(参见扩展模块线程安全。)
  • 如果可能性足够大,我们能做些什么来帮助扩展维护者缓解问题并享受在每个解释器 GIL 下使用的乐趣?
  • allow_all_extensions 有没有更好的(听起来更可怕的)名称?

延迟的功能

  • PyInterpreterConfig 选项,用于始终在新线程中运行解释器
  • PyInterpreterConfig 选项,用于将“主”线程分配给解释器,并且仅在该线程中运行

被拒绝的想法

<待定>

额外上下文

共享全局对象

我们正在解释器之间共享一些全局对象。这是一个实现细节,与全局变量整合而非本提案更相关,但它是一个足够重要的细节,需要在此解释。

另一种选择是永远不在解释器之间共享任何对象。为了实现这一点,我们必须解决所有静态类型的命运,并处理许多在公共 C-API 中公开的对象的兼容性问题。

这种方法引入了大量的额外复杂性和更高的风险,尽管原型设计已经证明了有效的解决方案。此外,它可能会导致性能损失。

不朽对象允许我们共享其他不可变的全局对象。这样我们就可以避免额外的开销。

C-API 中公开的对象

C-API(包括受限 API)公开了所有内置类型,包括内置异常,以及内置单例。异常以 PyObject * 的形式公开,但其余以静态值而非指针的形式公开。这是我们必须为每个解释器 GIL 解决的少数非平凡问题之一。

有了不朽对象,这就不再是个问题了。

整合运行时全局状态

正如上面CPython 运行时状态中提到的,正在进行一项积极的工作(与本 PEP 无关),旨在将 CPython 的全局状态整合到 _PyRuntimeState 结构中。几乎所有工作都涉及将该状态从全局变量中移出。该项目与本提案特别相关,因此下面将提供一些额外细节。

整合的好处

整合全局变量有多种好处:

  • 大大减少了 C 全局变量的数量(C 代码的最佳实践)
  • 此举将运行时状态不稳定或损坏的情况提请注意
  • 鼓励运行时状态使用方式更加一致
  • 更容易发现/识别 CPython 的运行时状态
  • 更容易以一致的方式静态分配运行时状态
  • 更好的运行时状态内存局部性

此外,上面间接好处中列出的所有好处也适用于此处,并且那里列出的相同项目也会受益。

工作量

需要移动的全局变量数量足够大,但大多数是 Python 对象,可以大量处理(如 Py_IDENTIFIER)。在几乎所有情况下,将这些全局变量移至解释器都是高度机械化的。这不需要巧妙,而是需要有人投入时间。

待移动的状态

其余全局变量可分类如下:

  • 全局对象
    • 静态类型(包括异常类型)
    • 非静态类型(包括堆类型、structseq 类型)
    • 单例(静态)
    • 单例(一次初始化)
    • 缓存对象
  • 非对象
    • 初始化后不会(或不太可能)更改
    • 仅在主线程中使用
    • 延迟初始化
    • 预分配缓冲区
    • 状态

这些全局变量分散在核心运行时、内置模块和标准库扩展模块之间。

要了解剩余全局变量的详细分类,请运行:

./python Tools/c-analyzer/table-file.py Tools/c-analyzer/cpython/globals-to-fix.tsv

已完成的工作

如前所述,这项工作已经进行了多年。以下是一些已经完成的事情:

  • 运行时初始化清理(参见 PEP 432 / PEP 587
  • 扩展模块隔离机制(参见 PEP 384 / PEP 3121 / PEP 489
  • 许多内置模块的隔离
  • 许多标准库扩展模块的隔离
  • 添加了 _PyRuntimeState
  • 不再有 _Py_IDENTIFIER()
  • 静态分配
    • 空字符串
    • 字符串字面量
    • 标识符
    • latin-1 字符串
    • 长度为 1 的字节
    • 空元组

工具

如前所述,有几个工具可以帮助识别全局变量并对其进行推理。

  • Tools/c-analyzer/cpython/globals-to-fix.tsv - 剩余全局变量列表
  • Tools/c-analyzer/c-analyzer.py
    • analyze - 识别所有全局变量
    • check - 如果存在任何未被忽略的不支持的全局变量,则失败
  • Tools/c-analyzer/table-file.py - 汇总已知全局变量

此外,对不支持的全局变量的检查已集成到 CI 中,以防止意外添加新的全局变量。

全局对象

可以安全地在解释器之间共享(无需 GIL)的全局对象可以保留在 _PyRuntimeState 上。对象不仅必须是实际不可变的(例如单例、字符串),而且即使引用计数也不能更改,才能保证安全。不朽对象 (PEP 683) 提供了这一点。(另一种选择是根本不共享对象,这会大大增加解决方案的复杂性,特别是对于在公共 C-API 中公开的对象。)

内置静态类型是全局对象的一种特殊情况,它们将被共享。除了一个部分外,它们实际上是不可变的:__subclasses__(又称 tp_subclasses)。我们预期内置类型的其他任何部分都不会改变,即使是 __dict__(又称 tp_dict)的内容也不会改变。

内置类型的 __subclasses__ 将通过将其设置为一个 getter 来处理,该 getter 在当前 PyInterpreterState 中查找该类型。

参考资料

相关内容

  • PEP 384 “定义稳定 ABI”
  • PEP 432 “重构 CPython 启动序列”
  • PEP 489 “多阶段扩展模块初始化”
  • PEP 554 “标准库中的多解释器”
  • PEP 573 “从 C 扩展方法访问模块状态”
  • PEP 587 “Python 初始化配置”
  • PEP 630 “隔离扩展模块”
  • PEP 683 “不朽对象,使用固定引用计数”
  • PEP 3121 “扩展模块初始化和终结”

来源: https://github.com/python/peps/blob/main/peps/pep-0684.rst

最后修改时间: 2024-06-04 17:05:36 GMT