5 июня 2025 года был принят PEP-0734. Судя по информации на официальном сайте, он является продолжением PEP-0554. Этот PEP предлагает добавить новый модуль interpreters для поддержки проверки, создания и запуска кода в нескольких интерпретаторах в текущем процессе. А если идти дальше, то он является продолжением PEP-0684, который предлагает один GIL на интерпретатор.

Несколько полноценных интерпретаторов работающих рядом. Какие плюсы?

  • Один процесс;

  • Один тред, но руками можно создавать еще;

  • Данные между интерпретаторами всегда передаются через сериализацию, аналогичную pickle, включая примитивные типы;

  • По GILу на интерпретатор, все еще можно получить плюшки настоящей многозадачности по сети;

  • Работает с asyncio.

GIL (Global Interpreter Lock) в Python — глобальная блокировка интерпретатора. Это механизм, встроенный в стандартную реализацию Python (CPython), который предотвращает одновременное выполнение байт-кода Python несколькими потоками.

Среди минусов — данный PEP значительно изменил C-код, и поэтому не всегда гарантируется стабильность C-расширений. Кстати, о том, как их создавать, я рассказывал в своей прошлой статье.

Есть несколько важных нетехнических аспектов про процесс создания данной фичи:

  • PEP-734 и Free-Threading делают очень похожие вещи – позволяют реализовывать настоящую многозадачность, но разными способами;

  • Изначально субинтерпретаторы появились в 3.10 в виде только C-API;

  • Есть отдельный PyPI пакет (https://pypi.org/project/interpreters-pep-734/) с данным кодом;

  • Python часть в виде PEP-734 был добавлен в 3.14 уже после feature freeze;

  • Изначально планировалось добавить его как модуль interpreters, однако в последний момент он стал concurrent.interpreters, вот тут доступно большое обсуждение.

PEP добавляет модуль interpreters (concurrent.interpreters). Этот включает объекты Interpreter, представляющие базовые интерпретаторы. Модуль также предоставляет базовый класс Queue (очереди) для связи между интерпретаторами.

Для пользователей будет простой API:

interp = interpreters.create()
try:
    interp.exec('print("Hello from PEP-554")')
finally:
    interp.close()

Прямо сейчас, если использовать Python 3.14, можно импортировать пакет concurrent.interpreters:

import concurrent.interpreters as interpreters

interp = interpreters.create()

a = 15
print(f"A in main: {a}")

try:
    interp.exec('print("Hello from PEP-554")\na = 10\nprint(f"A in subinterp: {a}")')
finally:
    interp.close()

Вывод при запуске:

A in main: 15
Hello from PEP-554
A in subinterp: 10

❯ Почему этот PEP важен?

Модуль interpreters предоставит высокоуровневый интерфейс для функциональности множественных интерпретаторов. Цель состоит в том, чтобы сделать существующую функцию множественных интерпретаторов CPython более доступной для кода Python. Это особенно актуально сейчас, когда CPython имеет GIL для каждого интерпретатора (PEP 684), и люди больше заинтересованы в использовании множественных интерпретаторов.

Без модуля stdlib пользователи ограничены C API , что ограничивает их возможности экспериментировать и использовать преимущества нескольких интерпретаторов.

Модуль будет включать базовый механизм для общения между интерпретаторов. Без него несколько интерпретаторов будут гораздо менее полезной функцией.

❯ Устройство

По сути, «интерпретатор» — это коллекция (по сути) всех состояний времени выполнения, которые потоки Python должны совместно использовать.

Процессы в питоне могут иметь один или больше потоков ОС, выполняющих python-код (или которые взаимодействуют с C API). Каждый из этих потоков работает с рантаймом CPython.

Интерпретаторы создаются через C API с помощью Py_NewInterpreterFromConfig() (или Py_NewInterpreter(), который является легкой оберткой вокруг Py_NewInterpreterFromConfig()). Эта функция делает следующее:

  1. Создает новое состояние;

  2. Создает новое состояние потока;

  3. Устанавливает состояние потока как текущее (текущее состояние необходимо для инициализации интерпретатора);

  4. Инициализирует состояние интерпретатора, используя состояние потока;

  5. Возвращает состояние потока (все еще актуальное).

Когда запускается процесс Python, он создает одно состояние интерпретатора («главный» интерпретатор) с одним состоянием потока для текущего потока ОС. Затем среда выполнения Python инициализируется с их использованием.

После инициализации скрипт или модуль или REPL выполняется с их помощью. Это выполнение происходит в модуле интерпретатора __main__.

Когда процесс завершает выполнение запрошенного кода Python или REPL в основном потоке ОС, среда выполнения Python завершается в этом потоке с использованием основного интерпретатора.

❯ C API

Внутри можно найти много различных C-модулей. Давайте разберем их подробнее.


Python/crossinterp.c

В данном файле находится API для управления действиями между изолированными интерпретаторами. Основа, в общем.

Некоторые функции мы опустим, если они малозначительные (по типу _Py_GetMainfile), вы их сможете просмотреть сами.

Основные функции:

  • runpy_run_path вызывает запуск runpy вместе с путем;

  • set_exc_with_cause создает исключение с причиной.

  1. Управление интерпретаторами:

    • _PyXI_NewInterpreter(): Создает новый изолированный интерпретатор

    • _PyXI_EndInterpreter(): Завершает работу интерпретатора

    • _Py_CallInInterpreter(): Выполняет функцию в другом интерпретаторе

  2. Межязыковые данные:

    • _PyXIData_t: Структура для передачи данных между интерпретаторами

    • _PyObject_GetXIData(): Преобразует объект в межъязыковой формат

    • _PyXIData_NewObject(): Воссоздает объект из межъязыковых данных

  3. Сериализация:

    • _PyPickle_GetXIData(): Использует pickle для сериализации объектов

    • _PyMarshal_GetXIData(): Использует marshal для сериализации кода

  4. Управление сессиями:

    • _PyXI_session: Сессия выполнения в другом интерпретаторе

    • _PyXI_Enter(): Начало сессии в другом интерпретаторе

    • _PyXI_Exit(): Завершение сессии

  5. Обработка ошибок:

    • _PyXI_excinfo: Сохранение информации об исключениях между интерпретаторами

    • _PyXI_failure: Унифицированная обработка сбоев

Среди особенностей реализации можно выделить изоляцию главного (__main__) модуля для каждого интерпретатора.

Все данные передаются безопасно, делиться простыми можно без необходимости использовать pickle. Для сложных объектов же потребуется сериализация.

Поддерживается асинхронное выполнение вызовов и обработка памяти через флаг _Py_PENDING_RAWFREE.

Кроме этого, не стоит забывать про обработку исключений. Происходит сериализация исключений через _PyXI_excinfo, трассировка преобразуется в TracebackException, Реализованы методы распространения ошибок между интерпретаторами.


Modules/_interpretersmodule.c

Данный модуль отвечает за низкоуровневый доступ к примитивам интерпретаторов. Определение самих интерпретаторов.

Модуль предоставляет низкоуровневый API для работы с интерпретаторами Python, включая:

  • Создание и уничтожение интерпретаторов

  • Управление изоляцией между интерпретаторами

  • Выполнение кода в разных интерпретаторах

  • Межъядерную передачу данных

  • Управление конфигурацией интерпретаторов

В коде можно увидеть C-функцию interp_create, которая является реализацией create(). Он создает новый интерпретатор с указанной конфигурацией интерпретатора. После можно увидеть interp_destroy для уничтожения объекта интерпретатора (в python это destroy()). Также есть list_all для получения списка всех интерпретаторов в текущем модуле, а также get_current и get_main методы для получения текущего и главного интерпретатора.

Также можно выделить отдельный список функций для выполнения кода: exec() для выполнения произвольного кода, run_string для выполнения строки кода, run_func() для выполнения тела функции, и call() для вызова callable-объекта (включая callable-классы) с аргументами).

Для межъядерного и многопроцессорного взаимодействия идет реализация безопасного разделения буферов между интерпретаторами:

typedef struct {
    PyObject base;
    Py_buffer *view;
    int64_t interpid;
} xibufferview;

Для сереализации и десереализации сложных объектов есть механизм _PyXIData. Он используется в качестве аргумента к функциям где идет работа со сложными объектами. А также есть поддержка разделяемых объектов данных через параметры shared.

Кроме того, есть функции управления состояниями интерпретаторов. Состояния потоков связаны с состояниями интерпретатора примерно так же, как связаны потоки и процессы ОС (на высоком уровне). Для начала, связь — один ко многим. Состояние потока принадлежит одному интерпретатору (и хранит указатель на него). Это состояние потока никогда не используется для другого интерпретатора. Однако в обратном направлении интерпретатор может иметь ноль или более состояний потоков, связанных с ним. Интерпретатор считается активным только в потоках ОС, где одно из его состояний потоков является текущим.

Функция set___main___attrs устанавливает атрибуты в __main__ модуль, а capture_exception нужна для захвата исключений, чтобы в последующем передавать их между интерпретаторами. И также есть метод is_shareable для проверки возможности разделять объекты.

Среди особенностей данного файла можно выделить безопасную работу с файлами. А также очистка состояний (module_clear, traverse_module_state), деаллокаторы (xibufferview_dealloc), и использование Py_buffer для работы с разделяемыми буферами.

Объекты интерпретаторов строго изолированы, существует переключение сессий через _PyXI_Enter и _PyXI_Exit. Ошибки изоляции также обрабатывается через unwrap_not_shareable.

Конфигурация интерпретаторов совместима с PyInterpreterConfig, а сам конфиг можно создать через метод new_config.

Также интерпретаторы имеют управляемый жизненный цикл, реализуемый через проверку готовности, блокировки удаления текущего интерпретатора, счетчики ссылок.

Модуль помечен как Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, что означает что отдельный GIL для каждого интепретатора поддерживается. Существуют специальные типы исключений (InterpreterError, NotShareableError). Совместимы с marshal для сереализации сложных объектов.


Modules/_interpqueuesmodule.c

Данный модуль отвечает за очередь обмена сообщениями между интерпретаторами. Очереди работают между интерпретаторами Python в одном процессе, используют глобальную память для хранения данных и поддерживают блокировки для синхронизации.

В этом модуле находится несколько структур.

Первая из них — это _queueitem, элемент очереди, связный список.

struct _queueitem;

typedef struct _queueitem {
    /* The interpreter that added the item to the queue.
       The actual bound interpid is found in item->data.
       This is necessary because item->data might be NULL,
       meaning the interpreter has been destroyed. */
    int64_t interpid;
    _PyXIData_t *data;
    unboundop_t unboundop;
    struct _queueitem *next;
} _queueitem;

Принцип работы заключается в том, что каждый элемент очереди (_queueitem) содержит:

  • interpid: идентификатор-отправитель

  • data: буфер данных (до 256 КБ без сериализации)

  • next: указатель на следующий элемент (FIFO)

А также передача данных осуществляется только через queue.put. Примитивы синхронизации заимствованы из threading.Lock.

Следующая — это сама очередь (FIFO — first in — first out, первый вошел — первый вышел).

typedef struct _queue {
    Py_ssize_t num_waiters;  // protected by global lock
    PyThread_type_lock mutex;
    int alive;
    struct _queueitems {
        Py_ssize_t maxsize;
        Py_ssize_t count;
        _queueitem *first;
        _queueitem *last;
    } items;
    struct _queuedefaults {
        xidata_fallback_t fallback;
        int unboundop;
    } defaults;
} _queue;

Количество «ожидающих» (защищено GIL), мьютекс, статус жизни, подструктура _queueitems с максимальным размером, числом, а также первым и последним элементом, подструктура _queuedefaults для данных по умолчанию.

Потом идет _queueref — ссылка на очередь:

struct _queueref;

typedef struct _queueref {
    struct _queueref *next;
    int64_t qid;
    Py_ssize_t refcount;
    _queue *queue;
} _queueref;

В ней находится объект следующей ссылки, ID очереди, количество ссылок и сама очередь в виде указателя.

И в конце структура _queues:

typedef struct _queues {
    PyThread_type_lock mutex;
    _queueref *head;
    int64_t count;
    int64_t next_id;
} _queues;

Мьютекс, ссылка на очередь в виде «головы», количество и next_id. _queues является глобальным реестром всех очередей.

Также в этом модуле задается управление данными: _PyXIData_t как контейнер для данных, и механизмы для сериализации и десериализации объектов. Кроме того, можно увидеть политику обработки «несвязанных» объектов, когда интерпретатор-источник уничтожен.

Модуль потокобезопасный (синхронизация идет через PyThread_type_lock, операции с очередями атомарные). Используются собственные глобальные аллокаторы памяти, есть очистка и счетчики ссылок.

Ну и естественно обработка ошибок: python-исключения об очередях, система кодов и конвертация сишных ошибок в python-исключения.

Если кратко, то вот API модуля:

  • create()/destroy() — управление очередями

  • put()/get() — основные операции

  • bind()/release() — управление ссылками

  • Вспомогательные методы (get_count(), is_full() и другие)


Modules/_interpchannelsmodule.c

Финальный модуль, набор примитивов. Этот код реализует низкоуровневый механизм межъядерных каналов для CPython, обеспечивающий передачу данных между интерпретаторами. Его ядром является структура globals, обеспечивающая централизованное управление:

static struct globals {
    PyMutex mutex;          // Глобальный мьютекс для синхронизации
    int module_count;        // Счётчик активных под-интерпретаторов
    _channels channels;      // Корневой контейнер каналов
} _globals = {0};

Глобальное состояние защищено мьютексом, предотвращающим race conditions при доступе к списку каналов. Структура _channels управляет жизненным циклом всех каналов процесса:

typedef struct _channels {
    PyThread_type_lock mutex;  // Мьютекс списка каналов
    _channelref *head;         // Связный список активных каналов
    int64_t numopen;           // Счётчик открытых каналов
    int64_t next_id;           // Генератор уникальных ID
} _channels;

Каждый канал представлен иерархией структур:

  1. _channelref — запись в глобальном реестре

  2. _channel_state — основное состояние канала

  3. _channelitem — элемент передачи данных

Элемент очереди сообщений инкапсулирует передаваемые данные и метаинформацию:

typedef struct _channelitem {
    int64_t interpid;      // ID интерпретатора-источника
    _PyXIData_t *data;     // Кросс-интерпретационные данные
    _waiting_t *waiting;   // Семафор синхронизации
    unboundop_t unboundop; // Обработчик несвязанных объектов
    struct _channelitem *next; // Следующий элемент
} _channelitem;

Ключевая структура _channel_state управляет внутренним состоянием канала:

typedef struct _channel {
    PyThread_type_lock mutex;   // Локальный мьютекс
    _channelqueue *queue;       // Очередь сообщений (FIFO)
    _channelends *ends;         // Реестр интерпретаторов
    struct {
        unboundop_t unboundop;  // Стандартный обработчик объектов
        xidata_fallback_t fallback; // Fallback-сериализация
    } defaults;
    int open;                   // Флаг состояния
    struct _channel_closing *closing; // Состояние закрытия
} _channel_state;

Особое внимание уделено двухфазному закрытию, fallback-сериализации и автоматическому разрешению ссылок. Все это вместе гарантирует безопасное завершение при параллельных операций, обработку объектов вне стандартного XI-формата.

Экспортируемый тип channelid предоставляет интерфейс для Python:

typedef struct channelid {
    PyObject_HEAD
    int64_t cid;       // Уникальный ID канала
    int end;           // Роль конечной точки
    int resolve;       // Флаг авторазрешения
    _channels *channels; // Ссылка на контейнер
} channelid;

Как механизм, мьютексты иерархичны: из глобального в список каналов и дальше в локальный.

Операции атомарные (неделимые), существуют таумауты блокировок для предотвращения взаимных блокировок (извиняюсь за тавтологию).

И естественно не стоит забывать все это чистить — сборка мусора автоматическая при каждом уничтожении интерпретатора.

❯ Принцип работы передачи:

Отправка:

_PyXIData_t *data = xi_data_serialize(obj);  // Сериализация
_channelitem *item = create_item(data);       // Создание элемента
append_to_queue(queue, item);                 // Инъекция в очередь
signal_receivers(waiting);                    // Уведомление получателей

Получение:

_channelitem *item = pop_from_queue(queue);   // Извлечение элемента
if (!item) wait_with_timeout(mutex, timeout); // Блокировка при пустой очереди
PyObject *obj = xi_data_deserialize(item->data); // Десериализация

Закрытие:

channel->open = 0;                          // Установка флага
broadcast_closing(channel->waiting);         // Оповещение ждущих потоков
schedule_async_cleanup(channel);             // Асинхронная очистка

Система обработки ошибок преобразует коды системных вызовов (например EAGAIN) в Python-исключения, используя механизм PyErr_SetFromErrno. Для критических секций применяется паттерн Py_BEGIN_CRITICAL_SECTION с гарантией освобождения ресурсов. Реализация обеспечивает строгую изоляцию интерпретаторов через сериализацию объектов в независимое от GC представление.

❯ О модуле

Почитать об мотивации и о том как работает PEP можно по этой ссылке.

Модуль interpreters доступен в Python 3.14, но само нахождения модуля изменено, теперь это concurrent.interpreters.

В нем можно найти следующие методы:

  • concurrent.interpreters.list_all() — возвращает список объектов интерпретаторов, один для каждого известного.

  • concurrent.interpreters.get_current() — возвращает объект интерпретатора для текущего запущенного.

  • concurrent.interpreters.get_main() — возвращает объект интерпретатора для главного интепретатора.

  • concurrent.interpreters.create() — инициализирует новый (idle) Python-интерпретатор и возвращает объект интерпретаторе для него.

Подробнее об объектах можно почитать на странице документации.

Пример использования:

import concurrent.interpreters as interpreters
from textwrap import dedent

interp = interpreters.create()

# Run in the current OS thread.
interp.exec('print("spam!")')

interp.exec("""if True:
    print('spam!')
    """)

interp.exec(dedent("""
    print('spam!')
    """))

def run():
    print('spam!')

interp.call(run)

# Run in new OS thread.
t = interp.call_in_thread(run)
t.join()

Для Python 3.12+ есть еще PyPI-пакет interpreters-pep-734:

try:
    import interpreters
except ModuleNotFoundError:
    from interpreters_backports import interpreters

try:
    import interpreters.queues
except ModuleNotFoundError:
    import interpreters_backports.interpreters.queues
    from interpreters_backports import interpreters

try:
    from interpreters import channels
except ModuleNotFoundError:
    from interpreters_experimental.interpreters import channels

try:
    from concurrent.futures import ThreadPoolExecutor
except ModuleNotFoundError:
    from interpreters_backports.concurrent.futures import ThreadPoolExecutor

❯ А подробнее?

Процесс один, но интерпретаторов несколько и у каждого свой GIL. Все они делят одну выделенную память. Для защиты от перетерания данных разными интерпретаторами используется pickle, он не допускается мутация одной памяти из разных источников (чтобы не конфликтовали интерпретаторы).

Применять их можно аналогично CSP из Golang (если реализовать шедулдер).

Про использование неизменяемых данных в субинтерпретаторах можно посмотреть в докладе Юрия Селиванова.

Также на PyCON US-24 презентовали функционал подинтепретаторов и free-threading. Видео-доклад можно посмотреть здесь.

Субинтерпретаторы не управляются ОС, существуют в одном процессе. Потоки ОС могут быть привязаны к разным интерпретаторам, и если интерпретатор использует свой GIL (PEP-684), его потоки не блокируются GIL других интерпретаторов.

Фактически, подинтерпретатор — это отдельное пространство имен, которое может иметь отдельный GIL. Изолированный от других подинтерпретаторов.

А может и не иметь:

PyInterpreterConfig config = {
          .use_main_obmalloc = 0,
          .allow_fork = 0,
          .allow_exec = 0,
          .allow_threads = 1,
          .allow_daemon_threads = 0,
          .check_multi_interp_extensions = 1,
          .gil = PyInterpreterConfig_OWN_GIL,
      };

.gil = PyInterpreterConfig_OWN_GIL может быть PyInterpreterConfig_SHARED_GIL.

Согласно PEP:

The interpreters module will provide a high-level interface to the multiple interpreter functionality. The goal is to make the existing multiple-interpreters feature of CPython more easily accessible to Python code. This is particularly relevant now that CPython has a per-interpreter GIL (PEP 684) and people are more interested in using multiple interpreters.

Использование подинтепретаторов может дать буст к скорости благодаря отдельному GIL. И мы так плавно переходим к бенчмаркам.

❯ Бенчмарк

Код замера я взял отсюда (потребуется установка pyperf и httpx).

Бенчмарк использует IO-bound и CPU-bound задачи. Он запускает простую версию, Threading GIL/NoGIL, через мультипроцессинг и сами подинтерпретаторы.

IO-bound задача на Ryzen 7 5825u:

Regular: Mean +- std dev: 4.85 sec +- 0.48 sec
Threading: Mean +- std dev: 1.22 sec +- 0.19 sec
Multiprocessing: Mean +- std dev: 1.45 sec +- 0.26 sec
Subinterpreters: Meain +- std dev: 1.85 sec +- 0.30 sec

CPU-bound задача на Ryzen 7 5825u:

Regular: Mean +- std dev: 60.2 ms +- 0.6 ms
Threading: Mean +- std dev: 22.6 ms +- 0.7 ms
Multiprocessing: Mean +- std dev: 153 ms +- 3 ms
Subinterpreters: Mean +- std dev: 120.8 ms +- 4 ms

Для чистоты эксперимента, результаты на другой машине.

Здесь WORKLOADS были побольше чем в первом бенчмарке на ryzen, стали: WORKLOADS = [(1, 10000), (10001, 20000), (20001, 30000), (30001, 40000)]

CPU-bound задача на M2 Pro:

Regular: Mean +- std dev: 163 ms +- 1 ms
Threading with GIL: Mean +- std dev: 168 ms +- 2 ms
Threading NoGIL: Mean +- std dev: 48.7 ms +- 0.6 ms
Multiprocessing: Mean +- std dev: 73.4 ms +- 1.5 ms
Subinterpreters: Mean +- std dev: 44.8 ms +- 0.5 ms

IO-bound задача на M2 Pro:

Regular: Mean +- std dev: 1.45 sec +- 0.03 sec
Threading with GIL: Mean +- std dev: 384 ms +- 17 ms (~1/4 от 1.45s)
Threading NoGIL: Mean +- std dev: 373 ms +- 20 ms
Multiprocessing: Mean +- std dev: 687 ms +- 32 ms
Subinterpreters: Mean +- std dev: 547 ms +- 13 ms

Может показаться, что не так много производительности выдало. Но мы не учли что можно использовать асинхронность, многопоточность внутри подинтерпретаторов. В итоге отличный функционал для длинных задач, когда нужно использовать много или важна изоляция. Пишите свои мнения в комментариях.

Можно увидеть что сабинтерпретаторы уступают в IO-bound задачах перед тредингом. Почему так происходит?

В субинтерпретаторах каждый вызов interp.exec() требует сериализировать данные, переключение сессий между интерпретаторами, создание нового GIL для каждой операции. Само создание интерпретаторов дорогое, и поэтому их лучше выбирать для CPU-bound — ибо они дают истинный параллелизм на нескольких ядрах.

Субинтерпретаторы показывают преимущество в CPU-bound задачах только при истинно параллельном выполнении (когда у каждого есть свой GIL). В текущем CPython (общий GIL) они проигрывают потокам из-за накладных расходов на создание и передачу данных.

Каждый вызов interp.exec() требует преобразования данных в межъядерный формат через _PyXIData_t. Для простых типов (int, str) это происходит быстро, но при передаче сложных объектов (словари, датаклассы) включается механизм сериализации, аналогичный pickle. В тестах с передачей 1000 словарей размером 1 КБ сериализация съедала 37% времени выполнения.

❯ Общие выводы

К просмотру рекомендую интервью CPython Core разработчика Никиты Соболева и разработчика модуля сабинтерпретаторов Эрика Сноу.

Погружаясь в историю и реализацию субинтерпретаторов, ясно одно — что это довольно фундаментальный сдвиг в архитектуре CPython. По сути можно достичь истинного паралеллизма через субинтерпретаторы.

Особенно интересует деталь что мы просто изолируем состояния интерпретаторов, выдавая каждому по своему GIL. В том интервью Эрик верно подметил, что «изоляция даёт нам концептуальное преимущество».

Но эта самая изоляция очень сложно далась — все из-за нюансов в виде:

  1. Immortal objects (PEP 683): Объекты вроде None или малых целых чисел стали «бессмертными» — их счётчик ссылок фиксируется на астрономическом значении, исключая гонки между интерпретаторами.

Кстати, именно поэтому (из-за PEP 683) sys.getrefcount(X) где X — число от -5 до 256 включительно, показывает заоблачные значения, но стоит выйти за этот лимит, то число ссылок будет адекватным.

  1. Статические типы: Проблема изменяемых атрибутов (dict, subclasses) решена через перенаправление запросов в per-interpreter хранилища.

  2. Модули расширений: Требуют перехода на многофазную инициализацию (PEP 489) и heap-типы. Библиотеки вроде OpenSSL (через ssl модуль) — особый случай, где разделение состояния между интерпретаторами было проблематично. Но как известно, они уже побороли эту проблему.

Но не стоит забывать, что технология имеет свои минусы. Создание интерпретаторов весьма дорогое удовольствие — иногда легче обойтись потоками, многопроцессорностью или асинхронностью. Но все это вознаграждается, если уметь правильно использовать.

Особенно перспективна интеграция с asyncio. Каждый интерпретатор имеет свой собственный event loop, но пока что нет встроенной синхронизации между ними.

Субинтерпретаторы — не новая концепция. Их корни уходят в Python 1.5, где они возникли как ответ на проблему глобальных состояний. Идея инкапсуляции данных интерпретатора в отдельные структуры напоминает инженерные практики борьбы с «глобальным хаосом». Как отмечает Эрик, это логичное развитие: если потоки получили изолированные состояния (thread state), то и интерпретаторы заслужили аналогичное. Исторически вдохновением послужил TCL, но в Python эта функция десятилетиями оставалась «спящей» из-за недоступности из Python-кода и нарушений изоляции.

Эрик скептически относится насчёт массового использования субинтерпретаторов. В принципе, многие с ним согласятся, так как их ниша это библиотеки для высокоуровневых абстракций, веб-фреймворки, обработка данных. А также как альтернатива multiprocessing — ресурсы ОС при правильном использовании экономятся лучше, коммуникация в рамках процесса может быть быстрее.

Но успех технологии сабинтерпретаторов зависит также от адаптации C-расширений.

❯ Заключение

Код примеров и тестов работы с PEP-0734 доступен в моем репозитории.

Пользуясь правом небольшой рекламы, могу предложить вам подписаться на мой блог в телеграме и также на канал «Находки в опенсорсе». Если вам конечно статья понравилась и вы хотите видеть чуть больше.

Если вам понравилась статья, поделитесь ей с друзьями. А лучше приходите контрибьютить в python и прочие опенсорс проекты. Удачи!

Источники

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.

Комментарии (6)


  1. SystemSoft
    16.07.2025 09:12

    exec: ДА КАК ВЫ СМЕЕТЕ.

    eval: ну ладно, меня не заменят даже когда будет python 4.0.0.


  1. avkritsky
    16.07.2025 09:12

    Спасибо за статью!
    Разве сабинтерпретаторы не потеряют актуальность с free-threads? как минимум общая область видимости/память, без необходимости сериализации данных между потоками


    1. DrArgentum Автор
      16.07.2025 09:12

      Тут разные юзкейсы, интерпретаторы дороже в создании, но более функциональные. Отдельные пространства имен с собственным GIL (также можно и шарить главный GIL, судя по исходникам). Могут возникнуть проблемы при создании тредпула: GIL бы сильно мешал. А так за счет того что у каждого свой GIL и свой поток получается удобнее, не надо за общий GIL конкурировать


    1. zzzzzzerg
      16.07.2025 09:12

      как минимум общая область видимости/память, без необходимости сериализации данных между потоками

      Это как плюс так и минус - теперь вы в коде на питоне обязаны синхронизировать доступ к вашим данным - уточню, что ft-build даст вам структуры данных, которые не ломаются при конкурентном доступе, но все проблемы многопоточности теперь ложаться на ваши плечи.

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


      1. thethee
        16.07.2025 09:12

        memoryview можно передать, копируется только ссылка на память, изменять можно в обоих интерпретаторах


        1. thethee
          16.07.2025 09:12

          Тут есть пример с упоминанием PyBuffer

          https://t.me/opensource_findings/918