C API 扩展对自由线程的支持¶
从 3.13 版本开始,CPython 实验性地支持在禁用全局解释器锁 (GIL) 的配置下运行,该配置称为自由线程。本文档介绍了如何调整 C API 扩展以支持自由线程。
在 C 中识别自由线程构建¶
CPython C API 公开了 Py_GIL_DISABLED
宏:在自由线程构建中,它被定义为 1
,而在常规构建中则未定义。你可以使用它来启用仅在自由线程构建下运行的代码
#ifdef Py_GIL_DISABLED
/* code that only runs in the free-threaded build */
#endif
模块初始化¶
扩展模块需要明确表明它们支持在禁用 GIL 的情况下运行;否则,导入扩展程序将引发警告并在运行时启用 GIL。
有两种方法可以指示扩展模块是否支持在禁用 GIL 的情况下运行,具体取决于扩展程序是使用多阶段初始化还是单阶段初始化。
多阶段初始化¶
使用多阶段初始化的扩展(即,PyModuleDef_Init()
)应该在模块定义中添加一个 Py_mod_gil
插槽。如果你的扩展支持较旧版本的 CPython,则应该使用 PY_VERSION_HEX
检查来保护该插槽。
static struct PyModuleDef_Slot module_slots[] = {
...
#if PY_VERSION_HEX >= 0x030D0000
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
.m_slots = module_slots,
...
};
单阶段初始化¶
使用单阶段初始化的扩展(即,PyModule_Create()
)应该调用 PyUnstable_Module_SetGIL()
来指示它们支持在禁用 GIL 的情况下运行。该函数仅在自由线程构建中定义,因此你应该使用 #ifdef Py_GIL_DISABLED
来保护调用,以避免在常规构建中出现编译错误。
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
...
};
PyMODINIT_FUNC
PyInit_mymodule(void)
{
PyObject *m = PyModule_Create(&moduledef);
if (m == NULL) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}
通用 API 指南¶
大多数 C API 都是线程安全的,但也有一些例外情况。
结构字段:如果字段可能会被并发修改,则直接访问 Python C API 对象或结构中的字段不是线程安全的。
宏:诸如
PyList_GET_ITEM
和PyList_SET_ITEM
之类的访问宏不执行任何错误检查或锁定。如果容器对象可能会被并发修改,则这些宏不是线程安全的。借用引用:如果包含对象被并发修改,则返回借用引用的 C API 函数可能不是线程安全的。有关更多信息,请参阅有关借用引用的部分。
容器线程安全¶
诸如 PyListObject
、PyDictObject
和 PySetObject
之类的容器在自由线程构建中执行内部锁定。例如,PyList_Append()
将在附加项之前锁定列表。
PyDict_Next
¶
一个值得注意的例外是 PyDict_Next()
,它不锁定字典。如果字典可能会被并发修改,你应该使用 Py_BEGIN_CRITICAL_SECTION
来保护迭代字典。
Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
...
}
Py_END_CRITICAL_SECTION();
借用引用¶
一些 C API 函数返回借用引用。如果包含对象被并发修改,则这些 API 不是线程安全的。例如,如果列表可能会被并发修改,则使用 PyList_GetItem()
是不安全的。
下表列出了一些借用引用 API 及其返回强引用的替代项。
借用引用 API |
强引用 API |
---|---|
无(参见 PyDict_Next) |
|
并非所有返回借用引用的 API 都有问题。例如,PyTuple_GetItem()
是安全的,因为元组是不可变的。同样,并非所有上述 API 的使用都有问题。例如,PyDict_GetItem()
通常用于解析函数调用中的关键字参数字典;那些关键字参数字典实际上是私有的(其他线程无法访问),因此在该上下文中使用借用引用是安全的。
其中一些函数是在 Python 3.13 中添加的。你可以使用 pythoncapi-compat 包为较旧的 Python 版本提供这些函数的实现。
内存分配 API¶
Python 的内存管理 C API 在三个不同的分配域中提供函数:“raw”、“mem” 和 “object”。为了线程安全,自由线程构建要求仅使用 object 域分配 Python 对象,并且所有 Python 对象都使用该域分配。这与之前的 Python 版本不同,在之前的版本中,这只是一种最佳实践,而不是硬性要求。
注意
在你的扩展中搜索 PyObject_Malloc()
的用法,并检查分配的内存是否用于 Python 对象。使用 PyMem_Malloc()
分配缓冲区,而不是 PyObject_Malloc()
。
线程状态和 GIL API¶
Python 提供了一组用于管理线程状态和 GIL 的函数和宏,例如
即使GIL 被禁用,这些函数仍应在自由线程构建中使用以管理线程状态。例如,如果你在 Python 之外创建线程,则必须在调用 Python API 之前调用 PyGILState_Ensure()
,以确保线程具有有效的 Python 线程状态。
你应该继续在阻塞操作(例如 I/O 或锁获取)周围调用 PyEval_SaveThread()
或 Py_BEGIN_ALLOW_THREADS
,以允许其他线程运行循环垃圾收集器。
保护内部扩展状态¶
你的扩展可能具有以前受 GIL 保护的内部状态。你可能需要添加锁定来保护此状态。该方法将取决于你的扩展,但一些常见的模式包括
缓存:全局缓存是共享状态的常见来源。考虑使用锁来保护缓存,或者如果缓存对性能不是至关重要的,则在自由线程构建中禁用缓存。
全局状态:全局状态可能需要用锁保护,或者移动到线程局部存储。C11 和 C++11 提供了
thread_local
或_Thread_local
用于线程局部存储。
为自由线程构建构建扩展¶
C API 扩展需要专门为自由线程构建构建。wheels、共享库和二进制文件用 t
后缀表示。
pypa/manylinux 支持自由线程构建,带有
t
后缀,例如python3.13t
。如果您设置了 CIBW_FREE_THREADED_SUPPORT,则 pypa/cibuildwheel 支持自由线程构建。
有限 C API 和稳定 ABI¶
自由线程构建目前不支持有限 C API 或稳定 ABI。如果您使用 setuptools 构建扩展,并且当前设置了 py_limited_api=True
,您可以使用 py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED")
在使用自由线程构建时选择退出有限 API。
注意
您将需要专门为自由线程构建构建单独的 wheels。如果您当前使用稳定 ABI,您可以继续为多个非自由线程 Python 版本构建单个 wheel。
Windows¶
由于官方 Windows 安装程序的限制,从源代码构建扩展时,您需要手动定义 Py_GIL_DISABLED=1
。
另请参阅
移植扩展模块以支持自由线程:一个由社区维护的扩展作者移植指南。