multiprocessamento em python - o que é herdado pelo processo forkserver do processo pai?

Aug 15 2020

Estou tentando usar forkservere me deparei NameError: name 'xxx' is not definedcom processos de trabalho.

Estou usando o Python 3.6.4, mas a documentação deve ser a mesma, desdehttps://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodsisso diz que:

O processo do servidor fork é de thread único, então é seguro usar os.fork(). Nenhum recurso desnecessário é herdado.

Além disso, diz:

Melhor herdar do que conservar/descongelar

Ao usar os métodos spawn ou forkserver start, muitos tipos de multiprocessamento precisam ser selecionáveis ​​para que os processos filhos possam usá-los . No entanto, geralmente deve-se evitar enviar objetos compartilhados para outros processos usando pipes ou filas. Em vez disso, você deve organizar o programa para que um processo que precise de acesso a um recurso compartilhado criado em outro lugar possa herdá-lo de um processo ancestral.

Então, aparentemente, um objeto-chave no qual meu processo de trabalho precisa trabalhar não foi herdado pelo processo do servidor e passado para os trabalhadores, por que isso aconteceu? Eu me pergunto o que exatamente é herdado pelo processo forkserver do processo pai?

Aqui está a aparência do meu código:

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)

Obrigada!

melhor,

Respostas

1 alex_noname Aug 16 2020 at 19:32

Teoria

Abaixo está um trecho do blog Bojan Nikolic

As versões modernas do Python (no Linux) fornecem três maneiras de iniciar os processos separados:

  1. Fork() - fazendo o processo pai e continuando com a mesma imagem de processos no pai e no filho. Este método é rápido, mas potencialmente não confiável quando o estado pai é complexo

  2. Gerando os processos filhos, ou seja, fork()-ing e então execv para substituir a imagem do processo por um novo processo Python. Este método é confiável, mas lento, pois a imagem do processo é recarregada novamente.

  3. O mecanismo forkserver , que consiste em um servidor Python separado com um estado relativamente simples e que é fork()-ed quando um novo processo é necessário. Este método combina a velocidade de Fork()-ing com boa confiabilidade (porque o pai sendo bifurcado está em um estado simples).

Forkserver

O terceiro método, forkserver , é ilustrado abaixo. Observe que os filhos retêm uma cópia do estado forkserver. Este estado pretende ser relativamente simples, mas é possível ajustar isso através da API multiprocesso através do set_forkserver_preload()método.

Prática

Portanto, se você deseja que algo seja herdado pelos processos filho do pai, isso deve ser especificado no estado forkserver por meio de set_forkserver_preload(modules_names), que define uma lista de nomes de módulos para tentar carregar no processo forkserver. Dou um exemplo abaixo:

# 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)

Resultado:

# 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

E se não usarmos o pré-carregamento

...
    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

Então, depois de uma discussão inspiradora com Alex, acho que tenho informações suficientes para responder à minha pergunta: o que exatamente é herdado pelo processo forkserver do processo pai?

Basicamente, quando o processo do servidor iniciar, ele importará seu módulo principal e tudo o que estiver antes if __name__ == '__main__'será executado. É por isso que meu código não funciona, porque large_objectnão é encontrado em nenhum lugar no serverprocesso e em todos os processos de trabalho que se ramificam do serverprocesso .

A solução de Alex funciona porque large_objectagora é importada para o processo principal e para o servidor, de modo que todos os trabalhadores bifurcados do servidor também recebam arquivos large_object. Se combinado com set_forkserver_preload(modules_names)todos os trabalhadores pode até obter o mesmo large_object pelo que vi. O motivo do uso forkserveré explicado explicitamente nas documentações do Python e no blog de Bojan:

Quando o programa inicia e seleciona o método forkserver start, um processo do servidor é iniciado. A partir daí, sempre que um novo processo for necessário, o processo pai se conecta ao servidor e solicita que ele bifurque um novo processo. O processo do servidor fork é de thread único, então é seguro usar os.fork(). Nenhum recurso desnecessário é herdado .

O mecanismo forkserver, que consiste em um servidor Python separado com um estado relativamente simples e que é fork()-ed quando um novo processo é necessário. Este método combina a velocidade de Fork()-ing com boa confiabilidade (porque o pai sendo bifurcado está em um estado simples) .

Portanto, é mais do lado seguro da preocupação aqui.

Por outro lado, se você usar forkcomo método inicial, não precisará importar nada, pois todos os processos filhos obtêm uma cópia da memória do processo pai (ou uma referência se o sistema usar COW- copy-on-write, corrija-me se eu estiver errado). Neste caso, usando global large_objectvocê terá acesso large_objectdiretamente worker_func.

Pode forkservernão ser uma abordagem adequada para mim porque o problema que estou enfrentando é a sobrecarga de memória. Todas as operações que me atingem large_objectem primeiro lugar consomem muita memória, portanto, não quero recursos desnecessários em meus processos de trabalho.

Se eu colocar todos esses cálculos diretamente inherited.pycomo Alex sugeriu, ele será executado duas vezes (uma vez quando eu importei o módulo no main e uma vez quando o servidor o importar; talvez ainda mais quando os processos de trabalho nascerem?), isso é adequado se eu deseja apenas um processo seguro de thread único do qual os trabalhadores possam se bifurcar. Mas como estou tentando fazer com que os trabalhadores não herdem recursos desnecessários e obtenham apenas large_object, isso não funcionará. E colocar esses cálculos também não funcionará __main__, inherited.pypois agora nenhum dos processos os executará, incluindo principal e servidor.

Portanto, como conclusão, se o objetivo aqui é fazer com que os trabalhadores herdem recursos mínimos, é melhor dividir meu código em 2, fazer calculation.pyprimeiro, conservar o large_object, sair do interpretador e iniciar um novo para carregar o pickled large_object. Então eu posso simplesmente enlouquecer com forkou forkserver.