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

Python 增强提案

PEP 539 – CPython 中线程本地存储的新 C-API

作者:
Erik M. Bray,Masayuki Yamamoto
BDFL 代表:
Alyssa Coghlan
状态:
最终版
类型:
标准跟踪
创建:
2016年12月20日
Python 版本:
3.7
历史记录:
2016年12月16日,2017年8月31日,2017年9月8日
决议:
Python-Dev 消息

目录

摘要

该提案是在 CPython 中添加一个新的线程本地存储 (TLS) API,它将取代 CPython 解释器中现有 TLS API 的使用,同时弃用现有 API。新的 API 被命名为“线程特定存储 (TSS) API”(有关名称的由来,请参阅提议解决方案的理由)。

由于现有的 TLS API 仅在内部使用(文档中未提及,定义它的头文件 pythread.h 也不直接或间接包含在 Python.h 中),因此此提案可能仅影响 CPython,但也可能影响其他实现部分 CPython API 的解释器实现(PyPy?)。

这主要是因为旧的 API 使用 int 在所有平台上表示 TLS 密钥,这既不符合 POSIX 标准,在任何实际意义上也不具有可移植性 [1]

注意

在本文档中,“TLS”的首字母缩写词指的是线程本地存储,不应与“传输层安全”协议混淆。

规范

CPython 解释器内部使用的 TLS 当前 API 包括 6 个函数

PyAPI_FUNC(int) PyThread_create_key(void)
PyAPI_FUNC(void) PyThread_delete_key(int key)
PyAPI_FUNC(int) PyThread_set_key_value(int key, void *value)
PyAPI_FUNC(void *) PyThread_get_key_value(int key)
PyAPI_FUNC(void) PyThread_delete_key_value(int key)
PyAPI_FUNC(void) PyThread_ReInitTLS(void)

这些将被一组新的类似函数取代

PyAPI_FUNC(int) PyThread_tss_create(Py_tss_t *key)
PyAPI_FUNC(void) PyThread_tss_delete(Py_tss_t *key)
PyAPI_FUNC(int) PyThread_tss_set(Py_tss_t *key, void *value)
PyAPI_FUNC(void *) PyThread_tss_get(Py_tss_t *key)

规范还添加了一些新功能

  • 一个新的类型 Py_tss_t - 一个不透明类型,其定义可能取决于底层的 TLS 实现。它被定义为
    typedef struct {
        int _is_initialized;
        NATIVE_TSS_KEY_T _key;
    } Py_tss_t;
    

    其中 NATIVE_TSS_KEY_T 是一个宏,其值取决于底层的原生 TLS 实现(例如 pthread_key_t)。

  • Py_tss_t 变量的初始化程序,Py_tss_NEEDS_INIT
  • 三个新函数
    PyAPI_FUNC(Py_tss_t *) PyThread_tss_alloc(void)
    PyAPI_FUNC(void) PyThread_tss_free(Py_tss_t *key)
    PyAPI_FUNC(int) PyThread_tss_is_created(Py_tss_t *key)
    

    前两个是为 Py_tss_t 的动态(去)分配而需要的,特别是在使用 Py_LIMITED_API 构建的扩展模块中,由于该类型的实现是在构建时不透明的,因此无法对其进行静态分配。由 PyThread_tss_alloc 返回的值与使用 Py_tss_NEEDS_INIT 初始化的值处于相同状态,或者在动态分配失败的情况下为 NULLPyThread_tss_free 的行为包括先调用 PyThread_tss_delete,或者如果 key 参数指向的值为 NULL,则不执行任何操作。 PyThread_tss_is_created 如果给定的 Py_tss_t 已初始化(即通过 PyThread_tss_create),则返回非零值。

新的 TSS API 没有提供对应于 PyThread_delete_key_valuePyThread_ReInitTLS 的函数,因为这些函数仅在 CPython 现已弃用的内置 TLS 实现中需要;也就是说,这些函数的现有行为将按如下方式处理:PyThread_delete_key_value(key) 等效于 PyThread_set_key_value(key, NULL),而 PyThread_ReInitTLS() 是一个空操作 [8]

新的 PyThread_tss_ 函数与其原始对应函数几乎完全相同,但也有一些细微的差别:虽然 PyThread_create_key 不接受任何参数并返回一个 TLS 密钥作为 int,但 PyThread_tss_createPy_tss_t* 作为参数并返回一个 int 状态代码。如果 key 参数指向的值不是由 Py_tss_NEEDS_INIT 初始化的,则 PyThread_tss_create 的行为未定义。成功时返回的状态代码为零,失败时返回非零值。此规范未定义非零状态代码的含义。

类似地,其他 PyThread_tss_ 函数都传递了一个 Py_tss_t*,而以前则是按值传递密钥。此更改是必要的,因为作为不透明类型,Py_tss_t 类型在理论上可以是几乎任何大小。这对于使用 Py_LIMITED_API 构建的扩展模块尤其必要,因为在这些模块中,类型的尺寸是未知的。除了 PyThread_tss_free 之外,如果 key 参数指向的值为 NULL,则 PyThread_tss_ 的行为未定义。

此外,由于使用了 Py_tss_t 而不是 int,因此新 API 中的一些行为与现有 API 在密钥创建和删除方面有所不同。 PyThread_tss_create 可以对同一个密钥重复调用 - 对已初始化的密钥调用它是一个空操作,并立即返回成功。类似地,对未初始化的密钥调用 PyThread_tss_delete 也是如此。

PyThread_tss_delete 的行为被定义为将密钥的初始化状态更改为“未初始化” - 这允许例如在重新启动 CPython 解释器而不终止进程(例如将 Python 嵌入应用程序中)时将静态分配的密钥重置为合理的状态 [12]

旧的 PyThread_*_key* 函数将在文档中标记为已弃用,但不会生成运行时弃用警告。

此外,在 sizeof(pthread_key_t) != sizeof(int) 的平台上,PyThread_create_key 将立即返回失败状态,并且其他 TLS 函数在这些平台上都将为空操作。

API 规范比较

API 线程本地存储 (TLS) 线程特定存储 (TSS)
版本 现有
密钥类型 int Py_tss_t(不透明类型)
处理原生密钥 转换为 int 隐藏到内部字段中
函数参数 int Py_tss_t *
功能
  • 创建密钥
  • 删除密钥
  • 设置值
  • 获取值
  • 删除值
  • 重新初始化密钥(分叉后)
  • 创建密钥
  • 删除密钥
  • 设置值
  • 获取值
  • (设置 NULL 代替) [8]
  • (不必要) [8]
  • 动态(去)分配密钥
  • 检查密钥的初始化状态
密钥初始化程序 -1 作为密钥创建失败) Py_tss_NEEDS_INIT
需求 原生线程(从 CPython 3.7 开始 [9] 原生线程
限制 不支持原生 TLS 密钥以无法安全转换为 int 的方式定义的平台。 当定义了 Py_LIMITED_API 时,无法静态分配密钥。

示例

通过提议的更改,TSS 密钥的初始化方式如下

static Py_tss_t tss_key = Py_tss_NEEDS_INIT;
if (PyThread_tss_create(&tss_key)) {
    /* ... handle key creation failure ... */
}

然后可以像这样检查密钥的初始化状态

assert(PyThread_tss_is_created(&tss_key));

API 的其余部分与旧 API 的使用方法类似

int the_value = 1;
if (PyThread_tss_get(&tss_key) == NULL) {
    PyThread_tss_set(&tss_key, (void *)&the_value);
    assert(PyThread_tss_get(&tss_key) != NULL);
}
/* ... once done with the key ... */
PyThread_tss_delete(&tss_key);
assert(!PyThread_tss_is_created(&tss_key));

当定义了 Py_LIMITED_API 时,必须动态分配 TSS 密钥

static Py_tss_t *ptr_key = PyThread_tss_alloc();
if (ptr_key == NULL) {
    /* ... handle key allocation failure ... */
}
assert(!PyThread_tss_is_created(ptr_key));
/* ... once done with the key ... */
PyThread_tss_free(ptr_key);
ptr_key = NULL;

平台支持变更

将在 PEP 11 中添加一个新的“原生线程实现”部分,其中指出

  • 从 CPython 3.7 开始,所有平台都必须提供原生线程实现(例如 pthreads 或 Windows)来实现 TSS API。在没有原生线程的实现中发生的任何 TSS API 问题都将关闭为“不会修复”。

动机

这里主要的问题是原始 PyThread TLS API 定义的用于 TLS 值的密钥类型(int)。

原始 TLS API 是由 GvR 在 1997 年添加到 Python 中的,当时用于表示 TLS 值的密钥是 int,并且一直持续到撰写本文之时。这使用了 CPython 自己的 TLS 实现,该实现长期以来一直未使用,并且在 Python/thread.c 中基本保持不变。后来添加了在原生线程实现(pthreads 和 Windows)之上实现 API 的支持,并且内置实现被认为不再必要,并且此后已被移除 [9]

使用int来表示TLS密钥的问题在于,虽然它对CPython自身的TLS实现来说是可以的,并且碰巧与Windows兼容(Windows使用DWORD表示类似的数据),但它与POSIX标准的pthreads API不兼容,该标准将pthread_key_t定义为不透明类型,标准中没有进一步定义(如上所述的Py_tss_t[14]。这使得底层实现可以自由决定如何使用pthread_key_t值来查找线程特定的数据。

这通常不会成为Python API的问题,因为碰巧在Linux上pthread_key_t被定义为unsigned int,因此与Python的TLS API完全兼容——由pthread_create_key创建的pthread_key_t可以自由地转换为int并转换回来(好吧,不完全是,即使这样也有一些限制,正如issue #22206中指出的那样)。

但是,正如issue #25658指出的那样,至少有一些平台(即Cygwin、CloudABI,以及可能的其他平台)拥有其他现代且符合POSIX的pthreads实现,但与Python的API不兼容,因为它们的pthread_key_t的定义方式无法安全地转换为int。事实上,在添加pthreads TLS时,MvL就提出了遇到此问题的可能性[2]

有人可能会说,PEP 11对支持新的、非官方支持的平台(如CloudABI)提出了具体的要求,并且Cygwin支持的状态目前尚不明确。但是,这为支持其他与Linux和/或POSIX兼容的平台设置了非常高的障碍,而CPython在这些平台上可能“正常工作”,除了这一个障碍。CPython本身通过一个与POSIX不兼容的API设置了这个实现障碍(实际上对pthreads做出了无效的假设)。

提议解决方案的理由

使用不透明类型(Py_tss_t)作为TLS值的键,允许API与CPython支持的所有现有(POSIX和Windows)和未来(C11?)原生TLS实现兼容,因为它允许Py_tss_t的定义依赖于底层实现。

由于现有的TLS API在某些平台(例如Linux)的受限API[13]中可用,因此CPython努力在该级别也提供新的TSS API。但是请注意,当定义Py_LIMITED_API时,Py_tss_t定义将成为不透明结构体,因为将NATIVE_TSS_KEY_T作为受限API的一部分公开将阻止我们在不重新构建扩展模块的情况下切换原生线程实现。

必须引入一个新的API,而不是更改当前API的函数签名,以保持向后兼容性。新的API还更清晰地将这些相关的函数分组到一个名称前缀PyThread_tss_下。“tss”在名称中代表“线程特定存储”,并受到C11线程API的一部分“tss”API的命名和设计的影响[15]。但是,这绝不意味着与C11线程API兼容或支持C11线程API,或表示将来打算支持C11——它只是命名和设计的灵感来源。

包含特殊的初始化器Py_tss_NEEDS_INIT是由于并非所有原生TLS实现都定义了未初始化TLS密钥的哨兵值。例如,在Windows上,TLS密钥由DWORD(unsigned int)表示,其值必须被视为不透明[3]。因此,在Windows上没有无符号整数值可以安全地用于表示未初始化的TLS密钥。同样,POSIX也没有指定未初始化pthread_key_t的哨兵,而是依赖于pthread_once接口来确保每个进程只初始化一次给定的TLS密钥。因此,Py_tss_t类型包含一个显式的._is_initialized,可以独立于底层实现指示密钥的初始化状态。

PyThread_create_key更改为在使用pthreads的系统上立即返回失败状态,其中sizeof(int) != sizeof(pthread_key_t),旨在作为健全性检查:目前,PyThread_create_key可能会在这样的系统上报告初始成功,但尝试使用返回的密钥可能会失败。虽然在实践中,此错误在解释器初始化的早期发生,但最好在问题的源头(PyThread_create_key)立即失败,而不是在以后尝试使用无效密钥时失败。换句话说,这清楚地表明旧的API在无法可靠地使用它的平台上不受支持,并且不会努力添加此类支持。

被拒绝的想法

  • 不做任何事情:现状很好,因为它在Linux上有效,并且希望得到CPython支持的平台应该遵循PEP 11的要求。如上所述,如果CPython被要求进行更改以支持特定平台的特定特性或功能,这将是一个合理的论点,但在这种情况下,CPython的一个特性阻止了它在其他符合POSIX的平台上充分发挥其潜力。当前实现碰巧在Linux上工作是一个幸运的巧合,并且不能保证这种情况永远不会改变。
  • 受影响的平台只需配置Python --without-threads:这不再是选项,因为Python 3.7已删除了--without-threads选项[16]
  • 受影响的平台应该使用CPython内置的TLS实现,而不是原生TLS实现:这是对先前想法的一个更可接受的替代方案,事实上,之前有一个补丁就是这么做的[4]。但是,内置实现通常比原生实现“更慢且更笨拙”,这仍然不必要地阻碍了受影响平台上的性能。如果Python在没有原生TLS实现的情况下构建,至少还有一个其他模块(tracemalloc)也会出现问题。这个想法也不能被采用,因为内置实现已经被删除了。
  • 保留现有API,但通过提供从pthread_key_t值到int值的映射来解决此问题。已经进行过几次尝试([5][6]),但这会在目前不受此问题影响的平台(如Linux)上的性能关键代码中注入不必要的复杂性和开销。即使此解决方法的使用以平台兼容性为条件,它也引入了需要维护的平台特定代码,并且仍然存在先前被拒绝的想法中不必要地阻碍受影响平台上性能的问题。

实现

该问题的错误跟踪器上提供了补丁的初始版本[7]。自从迁移到GitHub以来,它的开发一直在Masayuki Yamamoto在GitHub上创建的CPython存储库的pep539-tss-api功能分支中继续进行[10]。一个正在进行中的PR可以在[11]中找到。

此参考实现不仅涵盖了新的API实现功能,还涵盖了将现有TLS API替换为新的TSS API所需的客户端代码更新。

参考文献和脚注


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

上次修改:2023-10-11 12:05:51 GMT