Программирование PythonКонкурентность и asyncioСтарший Python-разработчик многопроцессных сервисов

Делает ли multiprocessing.shared memory.SharedMemory запись в общий буфер атомарной между процессами?

Делает ли multiprocessing.shared_memory.SharedMemory запись в общий буфер атомарной между процессами?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

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

Это решает проблему дорогого обмена данными, но намеренно не объединяет обмен данными с синхронизацией. Управление моментом доступа остаётся ответственностью приложения.

Постановка проблемы

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

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

Подробное решение

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

from multiprocessing import Process, Lock from multiprocessing.shared_memory import SharedMemory def increment(name, lock): shm = SharedMemory(name=name) with lock: value = int.from_bytes(shm.buf[:8], "little") shm.buf[:8] = (value + 1).to_bytes(8, "little") shm.close() if __name__ == "__main__": shm = SharedMemory(create=True, size=8) shm.buf[:8] = (0).to_bytes(8, "little") lock = Lock() workers = [Process(target=increment, args=(shm.name, lock)) for _ in range(4)] for worker in workers: worker.start() for worker in workers: worker.join() shm.close() shm.unlink()

Здесь память общая, но изменение счётчика последовательно защищено блокировкой. GIL эту проблему не решает: процессы имеют разные интерпретаторы и разные GIL, а сама общая память не превращает несколько операций Python в одну атомарную операцию.

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

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

Ситуация из практики

Сервис обрабатывает изображения в нескольких процессах и хранит общий буфер результатов в SharedMemory. Каждый процесс записывает результат и увеличивает общий счётчик обработанных изображений.

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

Выбранный вариант — SharedMemory для самих изображений и отдельная блокировка только для метаданных и счётчика. Это сохраняет преимущество обмена большими данными без копирования, а узкая критическая секция ограничивает потери параллелизма. При завершении процессов сегмент закрывается, а владелец удаляет его, чтобы не оставить системный ресурс.

Что кандидаты часто упускают

  1. Достаточно ли блокировать только запись в SharedMemory?

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

  1. Заменяет ли multiprocessing.Lock атомарность самой памяти?

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

  1. Можно ли использовать обычный threading.Lock для защиты SharedMemory между процессами?

Нет, обычный threading.Lock предназначен для потоков внутри одного процесса и не является межпроцессным примитивом. Для процессов нужен объект вроде multiprocessing.Lock либо другой механизм межпроцессной синхронизации. При этом сам способ запуска процессов, например spawn или fork, влияет на то, как этот объект будет передан дочернему процессу, но не отменяет требования использовать общий межпроцессный примитив.