Поддержка расширений C API для Free Threading
Начиная с релиза 3.13, CPython имеет экспериментальную поддержку работы с отключенным глобальным интерпретатором блокировки (GIL) в конфигурации, называемой бесплатными потоками. Этот документ описывает, как адаптировать расширения C API для поддержки бесплатных потоков.
Идентификация сборки с бесплатными потоками в C
C API CPython предоставляет макрос 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 выделения памяти
C API управления памятью Python предоставляет функции в трех различных областях выделения: «сырые», «mem» и «объект». Для обеспечения потоковой безопасности в сборке с бесплатными потоками требуется, чтобы только объекты Python выделялись с использованием области объектов, и что все объекты Python выделялись с использованием этой области. Это отличается от предыдущих версий Python, где это было просто рекомендацией, а не жестким требованием.
Примечание
Ищите использование PyObject_Malloc() в вашем расширении и убедитесь, что выделенная память используется для объектов Python. Используйте PyMem_Malloc() для выделения буферов вместо PyObject_Malloc().
Состояние потока и API GIL
Python предоставляет набор функций и макросов для управления состоянием потока и GIL, например:
-
PyGILState_Ensure()иPyGILState_Release() -
PyEval_SaveThread()иPyEval_RestoreThread() -
Py_BEGIN_ALLOW_THREADSиPy_END_ALLOW_THREADS
Эти функции всё ещё должны использоваться в сборке с поддержкой свободных потоков для управления состоянием потока, даже когда GIL отключен. Например, если вы создаёте поток вне Python, вы должны вызвать PyGILState_Ensure() перед обращением к API Python, чтобы гарантировать, что у потока есть действительное состояние потока Python.
Вы должны продолжать вызывать PyEval_SaveThread() или Py_BEGIN_ALLOW_THREADS вокруг блокирующих операций, таких как ввод-вывод или получение блокировок, чтобы разрешить другим потокам выполнить сборку циклических мусор.
Защита внутреннего состояния расширения
Ваше расширение может иметь внутреннее состояние, которое ранее защищалось GIL. Вам может потребоваться добавить блокировку для защиты этого состояния. Подход будет зависеть от вашего расширения, но некоторые распространённые подходы включают:
- Кэши: глобальные кэши — распространённый источник совместного состояния. Рассмотрите возможность использования блокировки для защиты кэша или отключения его в сборке с поддержкой свободных потоков, если кэш не является критическим для производительности.
-
Глобальное состояние: глобальное состояние может потребовать защиты блокировкой или перемещения в хранилище локальных потоков. C11 и C++11 предоставляют
thread_localили_Thread_localдля хранилища локальных потоков.
Создание расширений для сборки с поддержкой свободных потоков
Расширения API C должны быть скомпилированы специально для сборки с поддержкой свободных потоков. Упаковки, общие библиотеки и двоичные файлы маркируются суффиксом t.
-
pypa/manylinux поддерживает сборку с поддержкой свободных потоков с суффиксом
t, напримерpython3.13t. - pypa/cibuildwheel поддерживает сборку с поддержкой свободных потоков, если вы установите CIBW_FREE_THREADED_SUPPORT.
Ограниченный API C и стабильная ABI
Сборка с поддержкой свободных потоков в настоящее время не поддерживает ограниченный API C или стабильную ABI. Если вы используете setuptools для компиляции расширения и в настоящее время устанавливаете py_limited_api=True, вы можете использовать py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED"), чтобы отказаться от ограниченного API при компиляции с поддержкой свободных потоков.
Примечание
Вам потребуется скомпилировать отдельные пакеты для сборки с поддержкой свободных потоков. Если вы в настоящее время используете стабильную ABI, вы можете продолжить компиляцию одного пакета для нескольких версий Python без поддержки свободных потоков.
Windows
Из-за ограничения официального установщика Windows вам необходимо вручную определить Py_GIL_DISABLED=1 при компиляции расширений из исходного кода.
См. также
Перенос модулей расширения для поддержки свободных потоков: поддерживаемое сообществом руководство по переносу для авторов расширений.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/free-threading-extensions.html