multiprocessamento: compartilhando um grande objeto somente leitura entre processos?

107

Os processos filhos gerados por meio de multiprocessamento compartilham objetos criados anteriormente no programa?

Eu tenho a seguinte configuração:

do_some_processing(filename):
    for line in file(filename):
        if line.split(',')[0] in big_lookup_object:
            # something here

if __name__ == '__main__':
    big_lookup_object = marshal.load('file.bin')
    pool = Pool(processes=4)
    print pool.map(do_some_processing, glob.glob('*.data'))

Estou carregando um grande objeto na memória e, em seguida, criando um pool de trabalhadores que precisam fazer uso desse grande objeto. O objeto grande é acessado somente leitura, não preciso passar modificações dele entre os processos.

Minha pergunta é: o objeto grande está carregado na memória compartilhada, como seria se eu gerasse um processo em unix / c, ou cada processo carrega sua própria cópia do objeto grande?

Atualização: para esclarecer mais - big_lookup_object é um objeto de pesquisa compartilhado. Eu não preciso dividir isso e processá-lo separadamente. Preciso manter uma única cópia dele. O trabalho que preciso dividir é ler muitos outros arquivos grandes e comparar os itens nesses arquivos grandes com o objeto de pesquisa.

Atualização adicional: o banco de dados é uma boa solução, o memcached pode ser uma solução melhor e o arquivo em disco (arquivar ou dbm) pode ser ainda melhor. Nesta questão, eu estava particularmente interessado em uma solução em memória. Para a solução final, estarei usando o hadoop, mas gostaria de ver se também posso ter uma versão na memória local.

Parand
fonte
seu código, conforme escrito, chamará marshal.loadpara pai e para cada filho (cada processo importa o módulo).
jfs
Você está certo, corrigido.
Paraná,
Para "in-memory local" e se você gostaria de evitar copiar o seguinte pode ser útil docs.python.org/library/…
jfs
compartilhar não. processos gerados (ie fork ou exec, por exemplo) é uma duplicata exata do processo de chamada ... mas em memória diferente. Para que um processo converse com outro, você precisa de comunicação entre processos ou leitura / gravação IPC em algum local de memória compartilhada .
ron

Respostas:

50

"Os processos filhos gerados por meio de multiprocessamento compartilham objetos criados anteriormente no programa?"

Não (python antes de 3.8) e Sim em 3.8 ( https://docs.python.org/3/library/multiprocessing.shared_memory.html#module-multiprocessing.shared_memory )

Os processos têm espaço de memória independente.

Solução 1

Para fazer o melhor uso de uma grande estrutura com muitos trabalhadores, faça isso.

  1. Escreva cada trabalhador como um "filtro" - lê os resultados intermediários do stdin, funciona, escreve os resultados intermediários no stdout.

  2. Conecte todos os trabalhadores como um pipeline:

    process1 <source | process2 | process3 | ... | processn >result

Cada processo lê, funciona e grava.

Isso é extremamente eficiente, pois todos os processos estão sendo executados simultaneamente. As gravações e leituras passam diretamente por buffers compartilhados entre os processos.


Solução 2

Em alguns casos, você tem uma estrutura mais complexa - geralmente uma estrutura "fan-out". Neste caso, você tem um pai com vários filhos.

  1. Pai abre dados de origem. O pai bifurca vários filhos.

  2. O pai lê a fonte, distribui partes da fonte para cada filho em execução simultaneamente.

  3. Quando o pai chegar ao fim, feche o cano. O filho obtém o fim do arquivo e termina normalmente.

As partes infantis são agradáveis ​​de escrever porque cada criança simplesmente lê sys.stdin.

O pai tem um pouco de trabalho de pés elaborado para desovar todos os filhos e reter os canos de maneira adequada, mas não é tão ruim.

Fan-in é a estrutura oposta. Vários processos em execução independente precisam intercalar suas entradas em um processo comum. O colecionador não é tão fácil de escrever, pois tem que ler de várias fontes.

A leitura de muitos canais nomeados geralmente é feita usando o selectmódulo para ver quais canais têm entrada pendente.


Solução 3

A pesquisa compartilhada é a definição de um banco de dados.

Solução 3A - carregue um banco de dados. Deixe os trabalhadores processarem os dados no banco de dados.

Solução 3B - crie um servidor muito simples usando werkzeug (ou similar) para fornecer aplicativos WSGI que respondem a HTTP GET para que os trabalhadores possam consultar o servidor.


Solução 4

Objeto do sistema de arquivos compartilhado. O sistema operacional Unix oferece objetos de memória compartilhada. Esses são apenas arquivos mapeados para a memória para que a troca de E / S seja feita em vez de mais leituras com buffer de convenção.

Você pode fazer isso a partir de um contexto Python de várias maneiras

  1. Escreva um programa de inicialização que (1) divida seu objeto gigante original em objetos menores e (2) inicie trabalhadores, cada um com um objeto menor. Os objetos menores podem ser objetos Python em conserva para economizar um pouquinho de tempo de leitura de arquivo.

  2. Escreva um programa de inicialização que (1) leia seu gigantesco objeto original e escreva um arquivo estruturado em página e codificado em bytes usando seekoperações para garantir que seções individuais sejam fáceis de encontrar com buscas simples. Isso é o que um mecanismo de banco de dados faz - divide os dados em páginas, torna cada página fácil de localizar por meio de um seek.

    Gerar trabalhadores com acesso a este grande arquivo estruturado por página. Cada trabalhador pode procurar as partes relevantes e aí fazer o seu trabalho.

S.Lott
fonte
Meus processos não são realmente complicados; eles são todos iguais, apenas processando diferentes partes de dados.
Paraná,
Eles geralmente podem ser estruturados como filtros. Eles leem seus dados, fazem seu trabalho e escrevem seus resultados para processamento posterior.
S.Lott
Gostei da sua solução, mas o que acontece com o bloqueio de E / S? E se o pai bloquear a leitura / gravação de / para um de seus filhos? O Select avisa que você pode escrever, mas não diz quanto. O mesmo vale para a leitura.
Cristian Ciupitu
Esses são processos separados - pais e filhos não interferem um no outro. Cada byte produzido em uma extremidade de um pipe está imediatamente disponível na outra extremidade para ser consumido - um pipe é um buffer compartilhado. Não tenho certeza do que sua pergunta significa neste contexto.
S.Lott
Posso verificar o que S.Lott disse. Eu precisava das mesmas operações feitas em um único arquivo. Portanto, o primeiro trabalhador executou sua função em cada linha com o número% 2 == 0 e salvou em um arquivo, e enviou as outras linhas para o próximo processo canalizado (que era o mesmo script). O tempo de execução caiu pela metade. É um pouco hacky, mas a sobrecarga é muito mais leve do que map / poop no módulo de multiprocessamento.
Vince
36

Os processos filhos gerados por meio de multiprocessamento compartilham objetos criados anteriormente no programa?

Depende. Para variáveis ​​globais somente leitura, muitas vezes isso pode ser considerado (além da memória consumida), caso contrário, não deveria.

a documentação do multiprocessamento diz:

Better to inherit than pickle/unpickle

No Windows, muitos tipos de multiprocessamento precisam ser selecionáveis ​​para que os processos filhos possam usá-los. No entanto, geralmente deve-se evitar o envio de objetos compartilhados para outros processos usando tubos ou filas. Em vez disso, você deve organizar o programa de forma que um processo que precisa de acesso a um recurso compartilhado criado em outro lugar possa herdá-lo de um processo ancestral.

Explicitly pass resources to child processes

No Unix, um processo filho pode fazer uso de um recurso compartilhado criado em um processo pai usando um recurso global. No entanto, é melhor passar o objeto como um argumento para o construtor do processo filho.

Além de tornar o código (potencialmente) compatível com o Windows, isso também garante que, enquanto o processo filho ainda estiver ativo, o objeto não será coletado como lixo no processo pai. Isso pode ser importante se algum recurso for liberado quando o objeto for coletado como lixo no processo pai.

Global variables

Tenha em mente que se o código executado em um processo filho tentar acessar uma variável global, o valor que ele vê (se houver) pode não ser o mesmo que o valor no processo pai no momento em que Process.start () foi chamado .

Exemplo

No Windows (CPU única):

#!/usr/bin/env python
import os, sys, time
from multiprocessing import Pool

x = 23000 # replace `23` due to small integers share representation
z = []    # integers are immutable, let's try mutable object

def printx(y):
    global x
    if y == 3:
       x = -x
    z.append(y)
    print os.getpid(), x, id(x), z, id(z) 
    print y
    if len(sys.argv) == 2 and sys.argv[1] == "sleep":
       time.sleep(.1) # should make more apparant the effect

if __name__ == '__main__':
    pool = Pool(processes=4)
    pool.map(printx, (1,2,3,4))

Com sleep:

$ python26 test_share.py sleep
2504 23000 11639492 [1] 10774408
1
2564 23000 11639492 [2] 10774408
2
2504 -23000 11639384 [1, 3] 10774408
3
4084 23000 11639492 [4] 10774408
4

Sem sleep:

$ python26 test_share.py
1148 23000 11639492 [1] 10774408
1
1148 23000 11639492 [1, 2] 10774408
2
1148 -23000 11639324 [1, 2, 3] 10774408
3
1148 -23000 11639324 [1, 2, 3, 4] 10774408
4
jfs
fonte
6
Hã? Como o z está sendo compartilhado entre os processos ??
cbare
4
@cbare: Boa pergunta! z de fato não é compartilhado, como mostra a saída com o sono. A saída sem repouso mostra que um único processo trata (PID = 1148) de todo o trabalho; o que vemos no último exemplo é o valor de z para este único processo.
Eric O Lebigot
Esta resposta mostra que znão é compartilhado. Isso responde à pergunta com: "não, pelo menos no Windows, uma variável pai não é compartilhada entre os filhos".
Eric O Lebigot
@EOL: tecnicamente você está correto, mas na prática, se os dados forem somente leitura (ao contrário do zcaso), eles podem ser considerados compartilhados.
jfs de
Só para esclarecer, a declaração Tenha em mente que se o código executado em um processo filho tenta acessar uma variável global ... na documentação 2.7 se refere ao Python em execução no Windows.
user1071847
28

S.Lott está correto. Os atalhos de multiprocessamento do Python fornecem efetivamente um bloco de memória duplicado separado.

Na maioria dos sistemas * nix, usar uma chamada de nível inferior para os.fork()fornecerá, de fato, memória de cópia na gravação, que pode ser o que você está pensando. AFAIK, em teoria, no mais simplista dos programas possível, você poderia ler esses dados sem tê-los duplicados.

No entanto, as coisas não são tão simples no interpretador Python. Os dados e metadados do objeto são armazenados no mesmo segmento de memória, portanto, mesmo que o objeto nunca mude, algo como um contador de referência para aquele objeto que está sendo incrementado causará uma gravação na memória e, portanto, uma cópia. Quase qualquer programa Python que está fazendo mais do que "imprimir 'olá'" causará incrementos na contagem de referência, então você provavelmente nunca perceberá o benefício da cópia na gravação.

Mesmo se alguém conseguisse hackear uma solução de memória compartilhada em Python, tentar coordenar a coleta de lixo entre os processos provavelmente seria muito doloroso.

Jarret Hardie
fonte
3
Nesse caso, apenas a região da memória da contagem de referências será copiada, não necessariamente os grandes dados somente leitura, não é?
kawing-chiu
7

Se você estiver executando no Unix, eles podem compartilhar o mesmo objeto, devido à forma como o fork funciona (ou seja, os processos filhos têm memória separada, mas é cópia-na-gravação, então pode ser compartilhado desde que ninguém o modifique). Tentei o seguinte:

import multiprocessing

x = 23

def printx(y):
    print x, id(x)
    print y

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=4)
    pool.map(printx, (1,2,3,4))

e obteve o seguinte resultado:

$ ./mtest.py
23 22995656
1
23 22995656
2
23 22995656
3
23 22995656
4

É claro que isso não prova que uma cópia não foi feita, mas você deve ser capaz de verificar isso em sua situação olhando a saída de pspara ver quanta memória real cada subprocesso está usando.

Jacob Gabrielson
fonte
2
E o coletor de lixo? O que acontece quando ele é executado? O layout da memória não muda?
Cristian Ciupitu
Essa é uma preocupação válida. Se isso afetaria Parand dependeria de como ele está usando tudo isso e quão confiável esse código deve ser. Se não estivesse funcionando para ele, eu recomendaria usar o módulo mmap para obter mais controle (supondo que ele queira seguir essa abordagem básica).
Jacob Gabrielson,
Publiquei uma atualização para o seu exemplo: stackoverflow.com/questions/659865/…
jfs
@JacobGabrielson: A cópia está feita. A questão original é se a cópia foi feita.
abhinavkulkarni
3

Processos diferentes têm espaços de endereço diferentes. Como executar diferentes instâncias do interpretador. É para isso que serve o IPC (comunicação entre processos).

Você pode usar filas ou canais para essa finalidade. Você também pode usar rpc sobre tcp se quiser distribuir os processos por uma rede posteriormente.

http://docs.python.org/dev/library/multiprocessing.html#exchanging-objects-between-processes

Vasil
fonte
2
Não acho que o IPC seria apropriado para isso; são dados somente leitura aos quais todos precisam acessar. Não faz sentido passá-lo entre os processos; na pior das hipóteses, cada um pode ler sua própria cópia. Estou tentando economizar memória por não ter uma cópia separada em cada processo.
Paraná,
Você pode ter um processo mestre delegando pedaços de dados para trabalhar em outros processos escravos. Os escravos podem solicitar dados ou enviar dados. Desta forma, nem todo processo terá uma cópia de todo o objeto.
Vasil
1
@Vasil: E se cada processo precisar de todo o conjunto de dados e estiver apenas executando uma operação diferente nele?
Will
1

Não está diretamente relacionado ao multiprocessamento em si, mas pelo seu exemplo, parece que você poderia usar apenas o módulo shelve ou algo parecido. O "big_lookup_object" realmente precisa estar completamente na memória?


fonte
Bom ponto, não comparei diretamente o desempenho do disco com o da memória. Achei que haveria uma grande diferença, mas na verdade não testei.
Paris,
1

Não, mas você pode carregar seus dados como um processo filho e permitir que ele compartilhe seus dados com outros filhos. ver abaixo.

import time
import multiprocessing

def load_data( queue_load, n_processes )

    ... load data here into some_variable

    """
    Store multiple copies of the data into
    the data queue. There needs to be enough
    copies available for each process to access. 
    """

    for i in range(n_processes):
        queue_load.put(some_variable)


def work_with_data( queue_data, queue_load ):

    # Wait for load_data() to complete
    while queue_load.empty():
        time.sleep(1)

    some_variable = queue_load.get()

    """
    ! Tuples can also be used here
    if you have multiple data files
    you wish to keep seperate.  
    a,b = queue_load.get()
    """

    ... do some stuff, resulting in new_data

    # store it in the queue
    queue_data.put(new_data)


def start_multiprocess():

    n_processes = 5

    processes = []
    stored_data = []

    # Create two Queues
    queue_load = multiprocessing.Queue()
    queue_data = multiprocessing.Queue()

    for i in range(n_processes):

        if i == 0:
            # Your big data file will be loaded here...
            p = multiprocessing.Process(target = load_data,
            args=(queue_load, n_processes))

            processes.append(p)
            p.start()   

        # ... and then it will be used here with each process
        p = multiprocessing.Process(target = work_with_data,
        args=(queue_data, queue_load))

        processes.append(p)
        p.start()

    for i in range(n_processes)
        new_data = queue_data.get()
        stored_data.append(new_data)    

    for p in processes:
        p.join()
    print(processes)    
Mott The Tuple
fonte
-4

Para a plataforma Linux / Unix / MacOS, forkmap é uma solução rápida e suja.

Maxim Imakaev
fonte