Following system colour scheme Selected dark colour scheme Selected light colour scheme

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 用户可以在同一个进程中运行多个解释器。但是,同一进程中的解释器始终共享大量全局状态。这是错误的根源,随着越来越多的人使用此功能,其影响也在不断增加。此外,足够的隔离将促进真正的多核并行,其中解释器不再共享 GIL。本提案中概述的更改将导致这种级别的解释器隔离。

高级概述

从高级别来看,此提案以以下方式更改 CPython

  • 在提供足够隔离的情况下,停止在解释器之间共享 GIL
  • 添加几个新的解释器配置选项用于隔离设置
  • 防止不兼容的扩展导致问题

GIL

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

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

CPython 运行时状态

正确隔离解释器要求将大多数 CPython 运行时状态存储在PyInterpreterState结构中。目前,只有一部分存储在其中;其余部分位于 C 全局变量或_PyRuntimeState中。其中大部分将需要被移动。

这直接与一项持续多年的努力(多年)相吻合,即大大减少对全局变量的内部使用,并将运行时状态整合到_PyRuntimeStatePyInterpreterState中。(请参阅下面的整合运行时全局状态。)该项目本身具有重大意义,并且几乎没有争议。因此,虽然每解释器 GIL 依赖于该工作的完成,但该项目不应被视为本提案的一部分,而仅是一个依赖项。

其他隔离注意事项

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

但是,除了已经提到的内容(例如 GIL)之外,解释器仍然共享某些状态还有几种方式。

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

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

依赖于永生对象

PEP 683 引入了永生对象作为 CPython 内部特性。使用永生对象,我们可以在所有解释器之间共享任何其他不可变全局对象。因此,本 PEP 无需解决如何处理在公共 C-API 中公开的各种对象。它还简化了关于内置静态类型该怎么办的问题。(请参阅下面的全局对象。)

这两个问题都有备选方案,但使用永生对象一切都会更简单。如果 PEP 683 未被接受,那么本提案将更新备选方案。这使我们能够减少本提案中的噪音。

动机

我们在此处解决的基本问题是 CPython 运行时中缺乏真正的多核并行(对于 Python 代码)。GIL 是原因。虽然在实践中它通常不是问题,但至少它使 Python 的多核故事变得模糊,这使得 GIL 成为持续的干扰。

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

间接收益

每解释器 GIL 所需的大部分工作都有好处,无论如何这些任务都值得完成

  • 使多解释器行为更可靠
  • 导致修复了长期存在的运行时错误,否则这些错误不会被优先考虑
  • 一直在暴露(并激发修复)以前未知的运行时错误
  • 推动了更清晰的运行时初始化(PEP 432PEP 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 将当前分配器包装到一个钩子中(例如,stdlib 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 创建的解释器的默认值。不受限制的(现状)将继续通过 PyInterpreterConfig_LEGACY_INIT 提供,该宏已用于主解释器和 Py_NewInterpreter()。这不会改变。

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

在下面,我们多次提到了“主”解释器。这指的是在运行时初始化期间创建的解释器,其初始 PyThreadState 对应于进程的主线程。它承担了许多独特的责任(例如,处理信号),以及在运行时初始化/最终化期间发挥特殊作用。它通常(目前)也是唯一的解释器。(另请参阅 https://docs.pythonlang.cn/3/c-api/init.html#sub-interpreter-support。)

PyInterpreterConfig.own_gil

如果为 true1),则新解释器将拥有自己的“全局”解释器锁。这意味着新解释器可以在没有其他解释器干扰的情况下运行。这有效地释放了充分利用多核的限制。这是本 PEP 的根本目标。

如果为 false0),则新解释器将使用主解释器的锁。这是 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 时,该状态受到保护。如果没有,此类模块必须使用锁来包装对该状态的任何使用(例如,通过调用)。

目前尚不清楚 supports-multiple-interpreters 是否与 supports-per-interpreter-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 模块是否仅限于主解释器(如 signal 模块),或者我们是否会在解释器之间泄漏该全局状态(受细粒度锁保护)?
  • 根据“合并运行时全局状态”部分,分离出一个包含所有相关信息的 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