2025, Nov 22 06:03
Мутация против переприсваивания в multiprocessing.Manager: как делиться списками между процессами
Почему мутация списков в Python multiprocessing.Manager видна всем процессам, а переприсваивание — нет. Показываем надёжные способы замены и обновления.
Передача состояния между процессами в Python кажется простой — пока вы не попытаетесь целиком заменить общий контейнер. В случае с multiprocessing.Manager добавление элементов в общий список видно во всех процессах, а переприсвоение переменной на совершенно новый список — нет. Разница тонкая, но принципиальная: мутация распространяется, переприсвоение — нет.
Постановка проблемы
Следующий минимальный пример показывает эту разницу. Расширение управляемого списка видно во всех процессах, тогда как присваивание переменной нового списка нигде больше не отражается.
from time import sleep
from multiprocessing import Manager, Process
def writer_task(shared_seq):
sleep(1)
shared_seq.extend(["a", "b", "c"]) # изменение распространится
# shared_seq = ["a", "b", "c"] # изменение не распространится
def reader_task(shared_seq):
step = 0
while step < 8:
step += 1
print(f"{step}: {shared_seq}")
sleep(0.2)
def run_demo():
with Manager() as mgr:
shared_seq = mgr.list()
proc = Process(target=writer_task, args=(shared_seq,))
procs = [proc]
proc.start()
proc = Process(target=reader_task, args=(shared_seq,))
procs.append(proc)
proc.start()
for proc in procs:
proc.join()
print("---")
print(list(shared_seq))
if __name__ == "__main__":
run_demo()
Что на самом деле происходит и почему
Когда вы пишете shared_seq = ["a", "b", "c"], вы не изменяете управляемый список; вы лишь переприсваиваете локальную переменную shared_seq совершенно новому обычному списку Python. Сам управляемый объект не изменился — значит, распространять нечего.
Напротив, вызовы методов вроде .append или .extend изменяют (мутируют) сам управляемый список. Управляемые контейнеры, которые предоставляет multiprocessing.Manager, пересылают такие изменения через границы процессов. Это соответствует правилу из документации Python о прокси-объектах и практике разработчиков: выполняйте операции, которые мутируют управляемый объект, а не переприсваивайте переменные. Присваивание переменной никогда не мутирует объект. Важная тонкость: присваивание по ключу, например data[key] = value, — это мутация, потому что под капотом выполняется вызов метода (data.__setitem__(key, value)).
Краткая отсылка к официальной пометке об этом поведении: прокси-объекты. Практический вопрос тогда звучит так: «что считать мутацией?» Методы, меняющие содержимое объекта, — да; простое переприсвоение — нет.
Как исправить код
Если вам действительно нужно заменить содержимое управляемого списка на месте, делайте присваивание срезу — в этом случае изменяется сам объект списка, а не подменяется новым. Прокси сохраняется, а изменения распространяются:
import multiprocessing as mp
def in_place_overwrite(proxy_list):
proxy_list[:] = ["x", "y", "z"]
if __name__ == "__main__":
with mp.Manager() as mgr:
args = [mgr.list("abc")]
print(*args)
job = mp.Process(target=in_place_overwrite, args=args)
job.start()
job.join()
print(*args)
Это даёт ожидаемый переход от ["a", "b", "c"] к ["x", "y", "z"]. Суть в том, что выражение proxy_list[:] = ... мутирует существующий управляемый список, а не переприсваивает имя proxy_list.
Почему это важно
Параллельный код и без того сложен; невидимые пустые операции только усугубляют ситуацию. Непонимание разницы между мутацией управляемого контейнера и переприсвоением локальной переменной приводит к трудноуловимым багам и несогласованному состоянию между процессами. Тот же принцип действует и для общих словарей: используйте операции-мутации, включая присваивание по ключу, чтобы менять управляемый объект.
Ключевые выводы
Когда вы делитесь списками или словарями через multiprocessing.Manager, сосредотачивайтесь на операциях, которые мутируют управляемый объект. Вызовы методов .append, .extend и присваивание элементов распространяются. Обычное присваивание переменной создаёт новый обычный объект и не затрагивает управляемый. Если нужно полностью заменить содержимое, используйте приёмы «на месте», например присваивание срезу для списков. Чёткое различение этих случаев сэкономит время, убережёт от гонок состояний и сделает ваш параллельный код надёжнее.