многопроцессорность в Python - что наследуется процессом forkserver от родительского процесса?

Aug 15 2020

Пытаюсь использовать 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)

Спасибо!

Лучший,

Ответы

1 alex_noname Aug 16 2020 at 19:32

Теория

Ниже приведен отрывок из блога Бояна Николича.

Современные версии Python (в Linux) предоставляют три способа запуска отдельных процессов:

  1. Fork () - запускает родительские процессы и продолжает образ одного и того же процесса как в родительском, так и в дочернем. Этот метод быстрый, но потенциально ненадежный, когда родительское состояние сложное.

  2. Создание дочерних процессов, то есть fork () - ing, а затем execv для замены образа процесса новым процессом Python. Этот метод надежен, но медленный, так как образ процессов перезагружается заново.

  3. Механизм 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
1 sgyzetrov Aug 17 2020 at 03:00

Итак, после вдохновляющего обсуждения с Алексом я думаю, что у меня достаточно информации, чтобы ответить на мой вопрос: что именно наследуется процессом 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_objectin worker_func.

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

Если я помещу все эти вычисления напрямую, inherited.pyкак предложил Алекс, он будет выполнен дважды (один раз, когда я импортировал модуль в main, и один раз, когда сервер импортирует его; может быть, даже больше, когда родились рабочие процессы?), Это подходит, если я просто нужен однопоточный безопасный процесс, от которого работники могут разветвляться. Но поскольку я пытаюсь заставить рабочих не наследовать ненужные ресурсы, а только получать large_object, это не сработает. И положить эти расчеты __main__в inherited.pyне будет работать , так как в настоящее время ни один из процессов не будет выполнять их, в том числе главного и сервера.

Итак, в заключение, если цель здесь - заставить рабочих унаследовать минимальные ресурсы, мне лучше разбить свой код на 2, сделать calculation.pyсначала, обработать large_object, выйти из интерпретатора и запустить новый, чтобы загрузить маринованный large_object. Тогда я могу просто свихнуться с любым forkили forkserver.