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,该 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初始化的值处于相同状态,或者在动态分配失败的情况下为NULL。PyThread_tss_free的行为涉及预防性地调用PyThread_tss_delete,如果key参数指向的值为NULL,则不执行任何操作。PyThread_tss_is_created如果给定的Py_tss_t已初始化(即通过PyThread_tss_create),则返回非零值。
新的 TSS API 不提供与PyThread_delete_key_value和PyThread_ReInitTLS相对应的函数,因为这些函数仅适用于 CPython 现已废弃的内置 TLS 实现;也就是说,这些函数的现有行为处理如下:PyThread_delete_key_value(key)等同于PyThread_set_key_value(key, NULL),而PyThread_ReInitTLS()是一个空操作[8]。
新的PyThread_tss_函数几乎与其原始对应函数完全类似,只有一些微小差异:PyThread_create_key不带参数并返回一个int类型的 TLS 键,而PyThread_tss_create接受一个Py_tss_t*作为参数并返回一个int状态码。PyThread_tss_create的行为是未定义的,如果key参数指向的值未通过Py_tss_NEEDS_INIT初始化。返回的状态码成功时为零,失败时为非零。非零状态码的含义未由本规范另行定义。
同样,其他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 * |
| 功能 |
|
|
| 键初始化器 | (-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 早在 1997 年就由 GvR 添加到 Python 中,当时用于表示 TLS 值的键是一个int,一直延续到本文撰写之时。这使用了 CPython 自己的 TLS 实现,该实现长期未被使用,在 Python/thread.c 中基本没有改变。对基于原生线程实现(pthreads 和 Windows)的 API 实现的支持在很晚才添加,并且内置实现已被认为不再必要并已被移除[9]。
选择int来表示 TLS 键的问题在于,虽然它对于 CPython 自己的 TLS 实现来说没问题,并且恰好与 Windows 兼容(Windows 使用DWORD表示类似数据),但它与 POSIX pthreads API 标准不兼容,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再转回来(好吧,不完全是,即使这也有一些限制,如问题 #22206 所指出)。
然而,正如问题 #25658 指出的那样,至少有一些平台(即 Cygwin、CloudABI,但可能还有其他平台)具有现代且符合 POSIX 标准的 pthreads 实现,但由于其pthread_key_t的定义方式无法安全地转换为int,因此与 Python 的 API 不兼容。事实上,MvL 在添加 pthreads TLS 时就提出了遇到这个问题的可能性[2]。
可以说,PEP 11对支持新的、非官方支持的平台(如 CloudABI)提出了具体要求,并且 Cygwin 支持的现状目前尚不明确。然而,这为支持那些原本兼容 Linux 和/或 POSIX 并且 CPython 除此障碍外可能“只是工作”的平台造成了非常高的障碍。CPython 本身通过一个与 POSIX 不兼容(实际上对 pthreads 做出无效假设)的 API 施加了这种实现障碍。
拟议解决方案的理由
使用不透明类型(Py_tss_t)作为 TLS 值的键,使得 API 能够与所有当前(POSIX 和 Windows)和未来(C11?)CPython 支持的原生 TLS 实现兼容,因为它允许Py_tss_t的定义依赖于底层实现。
由于现有 TLS API 已在某些平台(例如 Linux)的[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 的意图——这只是命名和设计的影响。
包含特殊初始化器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更改为在sizeof(int) != sizeof(pthread_key_t)的 pthreads 系统上立即返回失败状态,旨在进行健全性检查:目前,PyThread_create_key在此类系统上可能会报告初始成功,但尝试使用返回的键可能会失败。尽管实际上这种失败在解释器初始化早期就会发生,但最好在问题源头(PyThread_create_key)立即失败,而不是在尝试使用无效键时稍后失败。换句话说,这清楚地表明,旧 API 在无法可靠使用的平台上不受支持,并且不会为此类支持付出任何努力。
被拒绝的想法
- 什么也不做:现状很好,因为它在 Linux 上运行良好,并且希望得到 CPython 支持的平台应遵循PEP 11的要求。如上所述,如果要求 CPython 进行更改以支持特定平台的特定怪癖或功能,这会是一个公平的论点,但在本例中,是 CPython 的一个怪癖阻止了它在其他符合 POSIX 标准的平台上充分发挥其潜力。当前实现在 Linux 上恰好有效是一个幸运的巧合,并且无法保证这种情况永远不会改变。
- 受影响的平台应只需配置 Python
--without-threads:这已不再是一个选项,因为--without-threads选项已针对 Python 3.7 移除[16]。 - 受影响的平台应使用 CPython 的内置 TLS 实现而不是原生 TLS 实现:这比上一个想法更可接受,事实上,有一个补丁就是这样做的[4]。然而,内置实现普遍比原生实现“更慢、更笨拙”,仍然不必要地阻碍了受影响平台上的性能。如果 Python 在没有原生 TLS 实现的情况下构建,至少还有一个模块(
tracemalloc)也会损坏。这个想法也无法采纳,因为内置实现已被移除。 - 保留现有 API,但通过提供从
pthread_key_t值到int值的映射来解决问题。为此进行了几次尝试([5],[6]),但这会在目前未受此问题影响的平台(如 Linux)上,向性能关键代码注入不必要的复杂性和开销。即使将这种变通方法的使能条件设为平台兼容性,它也会引入需要维护的平台特定代码,并且仍然存在之前被拒绝的想法所带来的问题,即不必要地阻碍受影响平台上的性能。
实施
此问题的错误跟踪器上提供了补丁的初始版本[7]。自从迁移到 GitHub 后,其开发一直在 Masayuki Yamamoto 的 CPython 存储库分支上的pep539-tss-api功能分支中继续[10]。一个正在进行中的 PR 可在[11]中找到。
这个参考实现不仅涵盖了新 API 的实现功能,还涵盖了将现有 TLS API 替换为新 TSS API 所需的客户端代码更新。
版权
本文档已置于公共领域。
参考文献和脚注
来源:https://github.com/python/peps/blob/main/peps/pep-0539.rst