Spec-Zone.ru › Python 3.14

Поддержка расширений 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, а также макросы, например PySequence_Fast_GET_SIZE, использующие объект, возвращённый функцией PySequence_Fast(), не выполняют проверку ошибок и не устанавливают блокировки. Эти макросы небезопасны с точки зрения потоков, если объект-контейнер может одновременно изменяться.
  • Заимствованные ссылки: функции 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()

PyCell_GET()

PyCell_Get()

Не все 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 для локального хранилища потока.

Критические секции

В сборке со свободной многопоточностью CPython предоставляет механизм, называемый «критическими секциями», для защиты данных, которые иначе защищались бы GIL. Авторам расширений, возможно, не придётся напрямую взаимодействовать с внутренней реализацией критических секций, однако понимание её поведения крайне важно при использовании определённых функций C API или управлении общим состоянием в сборке со свободной многопоточностью.

Что такое критические секции?

Концептуально критические секции служат уровнем предотвращения взаимных блокировок, построенным поверх обычных мьютексов. Каждый поток поддерживает стек активных критических секций. Когда потоку необходимо получить блокировку, связанную с критической секцией (например, неявно при вызове потокобезопасной функции C API, такой как PyDict_SetItem(), или явно с помощью макросов), он пытается захватить базовый мьютекс.

Использование критических секций

Основные API для работы с критическими секциями:

  • Py_BEGIN_CRITICAL_SECTION и Py_END_CRITICAL_SECTION — для блокировки одного объекта
  • Py_BEGIN_CRITICAL_SECTION2 и Py_END_CRITICAL_SECTION2 — для одновременной блокировки двух объектов

Эти макросы необходимо использовать согласованными парами и размещать в одной и той же области видимости C, поскольку они создают новую локальную область видимости. В сборках без свободной многопоточности эти макросы ничего не делают, поэтому их можно безопасно добавлять в код, который должен поддерживать оба типа сборок.

Обычно критическую секцию используют для блокировки объекта при обращении к его внутреннему атрибуту. Например, если у типа расширения есть внутреннее поле-счётчик, можно использовать критическую секцию при чтении или записи этого поля:

// read the count, returns new reference to internal count value
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(obj);
result = Py_NewRef(obj->count);
Py_END_CRITICAL_SECTION();
return result;

// write the count, consumes reference from new_count
Py_BEGIN_CRITICAL_SECTION(obj);
obj->count = new_count;
Py_END_CRITICAL_SECTION();

Как работают критические секции

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

Это поведение похоже на то, что происходит с GIL, когда поток выполняет блокирующий вызов. Основные отличия:

  • Критические секции действуют для отдельных объектов, а не глобально
  • Критические секции подчиняются стековому порядку в каждом потоке (макросы «начало» и «конец» обеспечивают его соблюдение, поскольку должны образовывать пару и находиться в одной области видимости)
  • Критические секции автоматически освобождают и повторно захватывают блокировки вокруг потенциально блокирующих операций

Предотвращение взаимных блокировок

Критические секции помогают избежать взаимных блокировок двумя способами:

  1. Если поток пытается получить блокировку, уже удерживаемую другим потоком, он сначала приостанавливает все свои активные критические секции, временно освобождая их блокировки
  2. После завершения блокирующей операции первой повторно захватывается только самая верхняя критическая секция

Это означает, что нельзя полагаться на вложенные критические секции для одновременной блокировки нескольких объектов, поскольку внутренняя критическая секция может приостановить внешние. Вместо этого используйте Py_BEGIN_CRITICAL_SECTION2, чтобы одновременно заблокировать два объекта.

Обратите внимание: описанные выше блокировки основаны только на PyMutex. Реализация критических секций не знает о других механизмах блокировки, которые могут использоваться, например о мьютексах POSIX, и не влияет на них. Также обратите внимание: хотя блокировка на любом PyMutex приводит к приостановке критических секций, освобождаются только мьютексы, входящие в состав критических секций. Если PyMutex используется без критической секции, она не будет освобождена, и описанное предотвращение взаимных блокировок работать не будет.

Важные замечания

  • Критические секции могут временно освобождать свои блокировки, позволяя другим потокам изменять защищённые данные. Будьте осторожны с предположениями о состоянии данных после операций, которые могут заблокировать поток.
  • Поскольку блокировки могут временно освобождаться (приостанавливаться), вход в критическую секцию не гарантирует исключительный доступ к защищённому ресурсу на протяжении всей секции. Если код внутри критической секции вызывает другую функцию, которая блокирует поток (например, получает другую блокировку или выполняет блокирующий ввод-вывод), все блокировки, удерживаемые потоком в рамках критических секций, будут освобождены. Это похоже на освобождение GIL во время блокирующих вызовов.
  • В любой момент гарантированно удерживаются только блокировки, связанные с последней открытой (самой верхней) критической секцией. Блокировки внешних, вложенных критических секций могли быть приостановлены.
  • С помощью этих API можно одновременно заблокировать не более двух объектов. Если необходимо блокировать больше объектов, потребуется переработать код.
  • Хотя критические секции не приводят к взаимной блокировке при попытке дважды заблокировать один и тот же объект, для такого сценария они менее эффективны, чем специализированные рекурсивные блокировки.
  • При использовании Py_BEGIN_CRITICAL_SECTION2 порядок объектов не влияет на корректность (реализация сама предотвращает взаимные блокировки), однако рекомендуется всегда блокировать объекты в одном и том же порядке.
  • Помните, что макросы критических секций предназначены прежде всего для защиты доступа к объектам Python, которые могут участвовать во внутренних операциях CPython, подверженных описанным выше сценариям взаимной блокировки. Для защиты исключительно внутреннего состояния расширения могут лучше подойти стандартные мьютексы или другие примитивы синхронизации.

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

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

  • pypa/manylinux поддерживает сборку со свободной многопоточностью с суффиксом t, например python3.14t.
  • pypa/cibuildwheel поддерживает сборку колёс для сборки Python 3.14 и новее со свободной многопоточностью.

Ограниченный 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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/howto/free-threading-extensions.html

Spec-Zone.ru

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