2025, Sep 19 10:03

Почему нельзя хранить объекты Python в shared_memory и что делать

Объясняем, почему объекты Python нельзя хранить в multiprocessing.shared_memory: ссылки и refcount ломаются. Показываем решение через сериализацию pickle.

Делиться сложным объектом Python между процессами через multiprocessing.shared_memory кажется соблазнительным, особенно если вы привыкли к приведению указателей в стиле C++. Но Python не позволяет переинтерпретировать произвольные байты в общей памяти как «живой» экземпляр произвольного класса. Ниже — почему так, что ломается в наивном подходе и как выглядит каноническое решение.

Постановка задачи

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

from multiprocessing import shared_memory
mem_region = shared_memory.SharedMemory(create=True, size=1024)
view = mem_region.buf
class Payload:
    def __init__(self, x, y):
        self.x = x
        self.y = y
item_a = Payload(1, 6)
view = item_a  # Это не помещает объект в общую память

А в другом процессе вы пытаетесь открыть этот регион по имени и восстановить объект:

from multiprocessing import shared_memory
opened_region = shared_memory.SharedMemory(name='psm_21467_46075')

Вопрос в том, как сделать так, чтобы переменная, скажем item_b, «указывала» на общий объект Payload — как в C++, где можно привести void * к нужному типу.

Что на самом деле не так

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

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

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

О субинтерпретаторах

Начиная с Python 3.12, к субинтерпретаторам можно обращаться из Python-кода. В пределах одного процесса прямой доступ к одной и той же памяти иногда может выглядеть рабочим. Есть код, демонстрирующий этот подход: https://github.com/jsbueno/extrainterpreters (не обновлён для Python 3.14). Даже так это не рекомендуемый путь для совместного использования живых объектов: проблема подсчёта ссылок остаётся, а атрибуты, содержащие ссылки на другие контейнеры или экземпляры, могут подвергаться параллельному доступу без защит вроде GIL или более тонких блокировок в free-threaded‑сборках.

Рабочий подход: сериализация

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

Поместите определение класса в модуль, доступный для импорта обоим процессам. Например, в файле typespec.py:

class Payload:
    def __init__(self, x, y):
        self.x = x
        self.y = y

В записывающем процессе:

from multiprocessing import shared_memory
from typespec import Payload
import pickle
region = shared_memory.SharedMemory(create=True, size=1024)
print(region.name)
obj_writer = Payload(5, 6)
blob = pickle.dumps(obj_writer)
region.buf[0:len(blob)] = blob

В читающем процессе:

from multiprocessing import shared_memory
import pickle
region_opened = shared_memory.SharedMemory("psm_ff9c5e26")
obj_reader = pickle.loads(region_opened.buf)

Явно импортировать модуль в читающем процессе не нужно. В сериализованной нагрузке есть информация __module__ для класса, и pickle при необходимости импортирует модуль перед восстановлением объекта.

Это дороже, чем работать с объектом «на месте» — переплата может быть на порядок‑другой, — но это стандартный и поддерживаемый способ.

Практический аспект

Если ваши объекты сочетают обычные атрибуты Python с большими числовыми буферами (например, данными под капотом датафрейма), можно сериализовать «лёгкую» структуру, а крупные буферы разделять без копирования ради скорости. Реализовать это непросто. Для начала посмотрите PEP 574. Проекты вроде Dask реализуют похожие идеи и могут быть быстрее, чем наивные схемы только с pickle.

Совместное использование «живого» состояния вместо байтов

Если нужно наблюдать и менять атрибуты объекта между процессами так, чтобы присваивание вида obj.a = 5 в одном процессе безопасно отражалось в другом, обратите внимание на инструменты из multiprocessing.manager. Это более высокий уровень для межпроцессного доступа к объектам, который может использовать дополнительный процесс как брокера.

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

Относиться к общей памяти как к универсальному контейнеру для произвольных экземпляров Python ненадёжно из‑за управляемой интерпретатором идентичности объектов, внутренних ссылок и подсчёта ссылок. Поддерживаемый путь — сериализация — сохраняет независимость процессов, избегает неопределённого поведения и интегрируется с примитивами multiprocessing и concurrent.futures, которые сами под капотом используют сериализацию. Да, есть накладные расходы, но корректность здесь важнее.

Выводы

Не пытайтесь приводить сырую общую память к экземпляру класса Python, как это делается с void * в C++. Используйте pickle: сериализуйте в совместную память и десериализуйте на другой стороне. Когда требуется «живое», согласованное состояние между процессами, рассмотрите multiprocessing.manager. Если ваши задачи совмещают метаданные с крупными бинарными нагрузками, изучите подходы в духе PEP 574 или используйте инструменты, уже оптимизирующие этот путь. И главное — опирайтесь на механизмы, которые предоставляет Python, вместо борьбы с объектной моделью рантайма.

Статья основана на вопросе на StackOverflow от Geremia и ответе от jsbueno.