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_ITEMPyList_SET_ITEM 之类的访问宏不执行任何错误检查或锁定。如果容器对象可能会被并发修改,则这些宏不是线程安全的。

  • 借用引用:如果包含对象被并发修改,则返回借用引用的 C API 函数可能不是线程安全的。有关更多信息,请参阅有关借用引用的部分。

容器线程安全

诸如 PyListObjectPyDictObjectPySetObject 之类的容器在自由线程构建中执行内部锁定。例如,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

PyList_GetItem()

PyList_GetItemRef()

PyDict_GetItem()

PyDict_GetItemRef()

PyDict_GetItemWithError()

PyDict_GetItemRef()

PyDict_GetItemString()

PyDict_GetItemStringRef()

PyDict_SetDefault()

PyDict_SetDefaultRef()

PyDict_Next()

无(参见 PyDict_Next

PyWeakref_GetObject()

PyWeakref_GetRef()

PyWeakref_GET_OBJECT()

PyWeakref_GetRef()

PyImport_AddModule()

PyImport_AddModuleRef()

并非所有返回借用引用的 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 后缀表示。

有限 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

另请参阅

移植扩展模块以支持自由线程:一个由社区维护的扩展作者移植指南。