Spec-Zone.ru › Python 3.13

Поддержка свободной многопоточности в расширениях C API

Начиная с выпуска 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

Примечание

В Windows этот макрос не определяется автоматически — его необходимо указать компилятору при сборке. Функцию sysconfig.get_config_var() можно использовать, чтобы определить, был ли макрос определён в текущем работающем интерпретаторе.

Инициализация модуля

Модули-расширения должны явно указывать, что они поддерживают работу с отключённой 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, возвращающий сильную ссылку

PyList_GetItem()

PyList_GetItemRef()

PyList_GET_ITEM()

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 выделения памяти

C API управления памятью Python предоставляет функции для трёх различных доменов выделения памяти: «raw», «mem» и «object». Для обеспечения потокобезопасности в сборке со свободной многопоточностью требуется выделять с помощью домена object только память для объектов 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, перед обращением к API Python необходимо вызвать PyGILState_Ensure(), чтобы убедиться, что у потока есть допустимое состояние потока Python.

При блокирующих операциях, таких как ввод-вывод или захват блокировок, продолжайте вызывать PyEval_SaveThread() или использовать Py_BEGIN_ALLOW_THREADS, чтобы другие потоки могли запускать сборщик циклического мусора.

Защита внутреннего состояния расширения

У вашего расширения может быть внутреннее состояние, ранее защищённое GIL. Для защиты этого состояния может потребоваться добавить блокировки. Подход зависит от конкретного расширения, но к распространённым шаблонам относятся:

  • Кэши: глобальные кэши часто являются источником общего состояния. Рассмотрите возможность защиты кэша блокировкой или его отключения в сборке со свободной многопоточностью, если кэш не критичен для производительности.
  • Глобальное состояние: глобальное состояние может потребовать защиты блокировкой или переноса в локальное хранилище потоков. В C11 и C++11 для локального хранилища потоков предусмотрены thread_local или _Thread_local.

Сборка расширений для сборки со свободной многопоточностью

Расширения C API необходимо специально собирать для сборки со свободной многопоточностью. Колёса, разделяемые библиотеки и двоичные файлы обозначаются суффиксом t.

  • pypa/manylinux поддерживает сборку со свободной многопоточностью с суффиксом t, например python3.13t.
  • pypa/cibuildwheel поддерживает сборку со свободной многопоточностью, если задать параметр CIBW_ENABLE со значением cpython-freethreading.

Ограниченный C API и стабильный ABI

Сборка со свободной многопоточностью в настоящее время не поддерживает ограниченный C API или стабильный 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–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/free-threading-extensions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API