многопроцессорность в Python - что наследуется процессом forkserver от родительского процесса?
Пытаюсь использовать forkserver
и столкнулся NameError: name 'xxx' is not defined
в рабочих процессах.
Я использую Python 3.6.4, но документация должна быть такой же, начиная с https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods он говорит, что:
Процесс сервера fork является однопоточным, поэтому использование os.fork () безопасно. Никакие ненужные ресурсы не наследуются.
Также там сказано:
Лучше наследовать, чем мариновать / рассыпать
При использовании методов запуска spawn или forkserver многие типы из многопроцессорной обработки должны быть выбираемыми, чтобы дочерние процессы могли их использовать . Однако обычно следует избегать отправки общих объектов другим процессам с использованием каналов или очередей. Вместо этого вы должны организовать программу так, чтобы процесс, которому требуется доступ к совместно используемому ресурсу, созданному где-то еще, мог унаследовать его от процесса-предка.
Итак, очевидно, что ключевой объект, над которым должен работать мой рабочий процесс, не был унаследован серверным процессом, а затем передан рабочим, почему это произошло? Интересно, что именно наследуется процессом forkserver от родительского процесса?
Вот как выглядит мой код:
import multiprocessing
import (a bunch of other modules)
def worker_func(nameList):
global largeObject
for item in nameList:
# get some info from largeObject using item as index
# do some calculation
return [item, info]
if __name__ == '__main__':
result = []
largeObject # This is my large object, it's read-only and no modification will be made to it.
nameList # Here is a list variable that I will need to get info for each item in it from the largeObject
ctx_in_main = multiprocessing.get_context('forkserver')
print('Start parallel, using forking/spawning/?:', ctx_in_main.get_context())
cores = ctx_in_main.cpu_count()
with ctx_in_main.Pool(processes=4) as pool:
for x in pool.imap_unordered(worker_func, nameList):
result.append(x)
Спасибо!
Лучший,
Ответы
Теория
Ниже приведен отрывок из блога Бояна Николича.
Современные версии Python (в Linux) предоставляют три способа запуска отдельных процессов:
Fork () - запускает родительские процессы и продолжает образ одного и того же процесса как в родительском, так и в дочернем. Этот метод быстрый, но потенциально ненадежный, когда родительское состояние сложное.
Создание дочерних процессов, то есть fork () - ing, а затем execv для замены образа процесса новым процессом Python. Этот метод надежен, но медленный, так как образ процессов перезагружается заново.
Механизм forkserver , который состоит из отдельного сервера Python с относительно простым состоянием и который запускается fork (), когда требуются новые процессы. Этот метод сочетает в себе скорость Fork () --ing с хорошей надежностью (поскольку родительский форк находится в простом состоянии).
Форксервер
Третий метод, forkserver , показан ниже. Обратите внимание, что дочерние элементы сохраняют копию состояния forkserver. Предполагается, что это состояние будет относительно простым, но его можно настроить с помощью многопроцессорного API с помощью
set_forkserver_preload()
метода.![]()
Упражняться
Таким образом, если вы хотите, чтобы дочерние процессы унаследовали что-то от родительского, это должно быть указано в состоянии forkserver с помощью set_forkserver_preload(modules_names)
, который задает список имен модулей, которые нужно попытаться загрузить в процессе forkserver. Приведу пример ниже:
# inherited.py
large_obj = {"one": 1, "two": 2, "three": 3}
# main.py
import multiprocessing
import os
from time import sleep
from inherited import large_obj
def worker_func(key: str):
print(os.getpid(), id(large_obj))
sleep(1)
return large_obj[key]
if __name__ == '__main__':
result = []
ctx_in_main = multiprocessing.get_context('forkserver')
ctx_in_main.set_forkserver_preload(['inherited'])
cores = ctx_in_main.cpu_count()
with ctx_in_main.Pool(processes=cores) as pool:
for x in pool.imap(worker_func, ["one", "two", "three"]):
result.append(x)
for res in result:
print(res)
Выход:
# The PIDs are different but the address is always the same
PID=18603, obj id=139913466185024
PID=18604, obj id=139913466185024
PID=18605, obj id=139913466185024
И если мы не используем предварительную загрузку
...
ctx_in_main = multiprocessing.get_context('forkserver')
# ctx_in_main.set_forkserver_preload(['inherited'])
cores = ctx_in_main.cpu_count()
...
# The PIDs are different, the addresses are different too
# (but sometimes they can coincide)
PID=19046, obj id=140011789067776
PID=19047, obj id=140011789030976
PID=19048, obj id=140011789030912
Итак, после вдохновляющего обсуждения с Алексом я думаю, что у меня достаточно информации, чтобы ответить на мой вопрос: что именно наследуется процессом forkserver от родительского процесса?
Обычно, когда запускается процесс сервера, он импортирует ваш основной модуль, и все, что было раньше, if __name__ == '__main__'
будет выполнено. Вот почему мой код не работает, потому что large_object
его нигде не найти в server
процессе и во всех тех рабочих процессах, которые ответвляются от server
процесса .
Решение Alex работает, потому что large_object
теперь импортируется как в основной, так и в серверный процесс, поэтому каждый рабочий, разветвленный с сервера, также получит large_object
. Если объединить со set_forkserver_preload(modules_names)
всеми рабочими, может даже получиться то же самое, large_object
что я видел. Причина использования forkserver
подробно объясняется в документации Python и в блоге Бояна:
Когда программа запускается и выбирает метод запуска forkserver, запускается процесс сервера. С этого момента всякий раз, когда требуется новый процесс, родительский процесс подключается к серверу и запрашивает его вилку для нового процесса. Процесс сервера fork является однопоточным, поэтому использование os.fork () безопасно. Никакие ненужные ресурсы не наследуются .
Механизм forkserver, который состоит из отдельного сервера Python с относительно простым состоянием и который запускается fork (), когда требуются новые процессы. Этот метод сочетает в себе скорость Fork () --ing с хорошей надежностью (поскольку родительский форк находится в простом состоянии) .
Так что здесь больше беспокойства.
Кстати, если вы используете fork
в качестве стартового метода, вам не нужно ничего импортировать, поскольку весь дочерний процесс получает копию памяти родительского процесса (или ссылку, если система использует COW- copy-on-write
, исправьте меня, если я неправильный). В этом случае с помощью global large_object
вы получите прямой доступ к large_object
in worker_func
.
forkserver
, Возможно , не является подходящим подходом для меня , потому что этот вопрос я облицовкой накладные расходы памяти. Все операции, которые меня large_object
привлекают, в первую очередь потребляют память, поэтому мне не нужны лишние ресурсы в моих рабочих процессах.
Если я помещу все эти вычисления напрямую, inherited.py
как предложил Алекс, он будет выполнен дважды (один раз, когда я импортировал модуль в main, и один раз, когда сервер импортирует его; может быть, даже больше, когда родились рабочие процессы?), Это подходит, если я просто нужен однопоточный безопасный процесс, от которого работники могут разветвляться. Но поскольку я пытаюсь заставить рабочих не наследовать ненужные ресурсы, а только получать large_object
, это не сработает. И положить эти расчеты __main__
в inherited.py
не будет работать , так как в настоящее время ни один из процессов не будет выполнять их, в том числе главного и сервера.
Итак, в заключение, если цель здесь - заставить рабочих унаследовать минимальные ресурсы, мне лучше разбить свой код на 2, сделать calculation.py
сначала, обработать large_object
, выйти из интерпретатора и запустить новый, чтобы загрузить маринованный large_object
. Тогда я могу просто свихнуться с любым fork
или forkserver
.