multiprocessing: condivisione di un grande oggetto di sola lettura tra i processi?


107

I processi figli generati tramite multiprocessing condividono oggetti creati in precedenza nel programma?

Ho la seguente configurazione:

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

Sto caricando un grande oggetto in memoria, quindi creo un pool di lavoratori che devono utilizzare quel grande oggetto. L'oggetto grande è accessibile in sola lettura, non ho bisogno di passare le modifiche tra i processi.

La mia domanda è: il grande oggetto è caricato nella memoria condivisa, come sarebbe se avessi generato un processo in unix / c, oppure ogni processo carica la propria copia dell'oggetto grande?

Aggiornamento: per chiarire ulteriormente - big_lookup_object è un oggetto di ricerca condiviso. Non ho bisogno di dividerlo ed elaborarlo separatamente. Devo conservarne una sola copia. Il lavoro di cui ho bisogno per dividerlo è leggere molti altri file di grandi dimensioni e cercare gli elementi in quei file di grandi dimensioni rispetto all'oggetto di ricerca.

Ulteriore aggiornamento: il database è una buona soluzione, memcached potrebbe essere una soluzione migliore e il file su disco (shelve o dbm) potrebbe essere ancora migliore. In questa domanda ero particolarmente interessato a una soluzione in memoria. Per la soluzione finale userò hadoop, ma volevo vedere se posso avere anche una versione in memoria locale.


il tuo codice come scritto chiamerà marshal.loadgenitore e ogni figlio (ogni processo importa il modulo).
jfs

Hai ragione, corretto.
Parand

Per "in memoria locale" e se desideri evitare di copiare, potrebbe essere utile docs.python.org/library/…
jfs

condividere n. processi generati (ad esempio fork o exec) sono un duplicato esatto del processo chiamante ... ma in una memoria diversa. Affinché un processo parli con un altro, è necessaria la comunicazione tra processi o la lettura / scrittura IPC in una posizione di memoria condivisa .
ron

Risposte:


50

"I processi figlio generati tramite multiprocessing condividono oggetti creati in precedenza nel programma?"

No (python prima della 3.8) e Sì nella 3.8 ( https://docs.python.org/3/library/multiprocessing.shared_memory.html#module-multiprocessing.shared_memory )

I processi hanno uno spazio di memoria indipendente.

Soluzione 1

Per utilizzare al meglio una grande struttura con molti lavoratori, fallo.

  1. Scrivi ogni worker come un "filtro": legge i risultati intermedi da stdin, funziona, scrive i risultati intermedi su stdout.

  2. Connetti tutti i lavoratori come una pipeline:

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

Ogni processo legge, funziona e scrive.

Ciò è notevolmente efficiente poiché tutti i processi vengono eseguiti contemporaneamente. Le scritture e le letture passano direttamente attraverso i buffer condivisi tra i processi.


Soluzione 2

In alcuni casi, hai una struttura più complessa, spesso una struttura "fan-out". In questo caso hai un genitore con più figli.

  1. Il genitore apre i dati di origine. Il genitore biforca un certo numero di bambini.

  2. Il genitore legge la sorgente, coltiva parti della sorgente a ogni figlio in esecuzione contemporaneamente.

  3. Quando il genitore raggiunge la fine, chiudi il tubo. Il bambino ottiene la fine del file e finisce normalmente.

Le parti del bambino sono piacevoli da scrivere perché ogni bambino legge semplicemente sys.stdin.

Il genitore ha un po 'di fantasia nel generare tutti i bambini e nel mantenere i tubi correttamente, ma non è poi così male.

Fan-in è la struttura opposta. Un certo numero di processi in esecuzione in modo indipendente devono intercalare i propri input in un processo comune. Il raccoglitore non è così facile da scrivere, poiché deve leggere da molte fonti.

La lettura da molte pipe con nome viene spesso eseguita utilizzando il selectmodulo per vedere quali pipe hanno input in sospeso.


Soluzione 3

La ricerca condivisa è la definizione di un database.

Soluzione 3A: carica un database. Consenti ai lavoratori di elaborare i dati nel database.

Soluzione 3B: creare un server molto semplice utilizzando werkzeug (o simile) per fornire applicazioni WSGI che rispondono a HTTP GET in modo che i lavoratori possano interrogare il server.


Soluzione 4

Oggetto filesystem condiviso. Il sistema operativo Unix offre oggetti di memoria condivisa. Questi sono solo file che vengono mappati alla memoria in modo che lo scambio di I / O venga eseguito invece di più letture memorizzate nel buffer della convenzione.

Puoi farlo da un contesto Python in diversi modi

  1. Scrivi un programma di avvio che (1) spezzi il tuo oggetto gigantesco originale in oggetti più piccoli e (2) inizi i worker, ciascuno con un oggetto più piccolo. Gli oggetti più piccoli potrebbero essere oggetti Python decapati per risparmiare un po 'di tempo di lettura del file.

  2. Scrivete un programma di avvio che (1) legga il vostro gigantesco oggetto originale e scriva un file strutturato in pagine e codificato in byte utilizzando seekoperazioni per garantire che le singole sezioni siano facili da trovare con semplici ricerche. Questo è ciò che fa un motore di database: suddividere i dati in pagine, rendere ogni pagina facile da individuare tramite un file seek.

    Genera lavoratori con accesso a questo file di grandi dimensioni strutturato a pagina. Ogni lavoratore può cercare le parti pertinenti e svolgere il proprio lavoro lì.


I miei processi non sono realmente fitlers; sono tutti uguali, elaborano solo pezzi di dati diversi.
Parand

Spesso possono essere strutturati come filtri. Leggono i dati, svolgono il proprio lavoro e scrivono il risultato per l'elaborazione successiva.
S.Lott

Mi piace la tua soluzione, ma cosa succede con il blocco I / O? Cosa succede se il genitore blocca la lettura / scrittura da / verso uno dei suoi figli? Select ti avvisa che puoi scrivere, ma non dice quanto. Lo stesso per la lettura.
Cristian Ciupitu

Questi sono processi separati: genitori e figli non interferiscono tra loro. Ogni byte prodotto a un'estremità di una pipe è immediatamente disponibile all'altra estremità per essere consumato: una pipe è un buffer condiviso. Non sono sicuro di cosa significhi la tua domanda in questo contesto.
S.Lott

Posso verificare cosa ha detto S.Lott. Avevo bisogno delle stesse operazioni fatte su un singolo file. Quindi il primo worker ha eseguito la sua funzione su ogni riga con il numero% 2 == 0 e l'ha salvata in un file e ha inviato le altre righe al processo piped successivo (che era lo stesso script). Il tempo di esecuzione è diminuito della metà. È un po 'complicato, ma l'overhead è molto più leggero di map / poop nel modulo multiprocessing.
Vince

36

I processi figli generati tramite multiprocessing condividono oggetti creati in precedenza nel programma?

Dipende. Per le variabili globali di sola lettura può essere spesso considerato così (a parte la memoria consumata) altrimenti non dovrebbe.

La documentazione di multiprocessing dice:

Better to inherit than pickle/unpickle

Su Windows molti tipi di multiprocessing devono essere selezionabili in modo che i processi figlio possano usarli. Tuttavia, si dovrebbe generalmente evitare di inviare oggetti condivisi ad altri processi utilizzando pipe o code. Dovresti invece organizzare il programma in modo che un processo che necessita dell'accesso a una risorsa condivisa creata altrove possa ereditarlo da un processo antenato.

Explicitly pass resources to child processes

Su Unix un processo figlio può utilizzare una risorsa condivisa creata in un processo genitore utilizzando una risorsa globale. Tuttavia, è meglio passare l'oggetto come argomento al costruttore per il processo figlio.

Oltre a rendere il codice (potenzialmente) compatibile con Windows, ciò garantisce anche che fintanto che il processo figlio è ancora attivo, l'oggetto non verrà raccolto nella spazzatura nel processo padre. Ciò potrebbe essere importante se alcune risorse vengono liberate quando l'oggetto viene sottoposto a garbage collection nel processo padre.

Global variables

Tieni presente che se il codice eseguito in un processo figlio tenta di accedere a una variabile globale, il valore che vede (se presente) potrebbe non essere lo stesso del valore nel processo padre al momento in cui è stato chiamato Process.start () .

Esempio

Su Windows (singola CPU):

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

Con 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

Senza 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

6
Eh? Come viene condiviso z tra i processi?
cbare

4
@cbare: bella domanda! z infatti non è condiviso, come mostra l'output con sleep. L'output senza sleep mostra che un singolo processo gestisce (PID = 1148) tutto il lavoro; quello che vediamo nell'ultimo esempio è il valore di z per questo singolo processo.
Eric O Lebigot

Questa risposta mostra che znon è condivisa. Questo quindi risponde alla domanda con: "no, almeno sotto Windows, una variabile genitore non è condivisa tra i figli".
Eric O Lebigot

@EOL: tecnicamente hai ragione ma in pratica se i dati sono di sola lettura (a differenza del zcaso) possono essere considerati condivisi.
jfs

Giusto per chiarire, l'affermazione Tenete a mente che se il codice eseguito in un processo figlio cerca di accedere a una variabile globale ... nei documenti 2.7 si fa riferimento a Python in esecuzione su Windows.
user1071847

28

S.Lott ha ragione. Le scorciatoie multiprocessing di Python ti danno effettivamente una porzione di memoria separata e duplicata.

Sulla maggior parte dei sistemi * nix, l'utilizzo di una chiamata di livello inferiore a os.fork()ti darà, infatti, memoria copia su scrittura, che potrebbe essere quello che stai pensando. AFAIK, in teoria, nel più semplicistico dei programmi possibile, potresti leggere da quei dati senza doverli duplicare.

Tuttavia, le cose non sono così semplici nell'interprete Python. I dati dell'oggetto ei metadati sono memorizzati nello stesso segmento di memoria, quindi anche se l'oggetto non cambia mai, qualcosa come un contatore di riferimenti per quell'oggetto che viene incrementato causerà una scrittura in memoria e quindi una copia. Quasi tutti i programmi Python che fanno qualcosa di più di "print 'hello'" causeranno incrementi del conteggio dei riferimenti, quindi probabilmente non ti renderai mai conto del vantaggio del copy-on-write.

Anche se qualcuno fosse riuscito a hackerare una soluzione di memoria condivisa in Python, provare a coordinare la raccolta dei rifiuti tra i processi sarebbe probabilmente piuttosto doloroso.


3
In questo caso verrà copiata solo la regione di memoria del conteggio ref, non necessariamente i dati di sola lettura di grandi dimensioni, non è vero?
kawing-chiu

7

Se stai utilizzando Unix, potrebbero condividere lo stesso oggetto, a causa di come funziona il fork (cioè, i processi figli hanno una memoria separata ma è copia su scrittura, quindi può essere condiviso finché nessuno lo modifica). Ho provato quanto segue:

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 ha ottenuto il seguente output:

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

Ovviamente questo non prova che una copia non sia stata fatta, ma dovresti essere in grado di verificarlo nella tua situazione guardando l'output di psper vedere quanta memoria reale sta usando ogni sottoprocesso.


2
E il netturbino? Cosa succede quando viene eseguito? Il layout della memoria non cambia?
Cristian Ciupitu

Questa è una preoccupazione valida. Se influenzerà Parand dipenderà da come sta usando tutto questo e da quanto affidabile deve essere questo codice. Se non funzionasse per lui, consiglierei di utilizzare il modulo mmap per un maggiore controllo (supponendo che voglia attenersi a questo approccio di base).
Jacob Gabrielson

Ho pubblicato un aggiornamento al tuo esempio: stackoverflow.com/questions/659865/…
jfs

@JacobGabrielson: La copia è stata fatta. La domanda originale riguarda se la copia è stata fatta.
abhinavkulkarni

3

Processi diversi hanno uno spazio degli indirizzi diverso. Come eseguire diverse istanze dell'interprete. Ecco a cosa serve IPC (comunicazione interprocesso).

È possibile utilizzare code o pipe per questo scopo. È inoltre possibile utilizzare rpc su tcp se si desidera distribuire i processi su una rete in un secondo momento.

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


2
Non credo che IPC sarebbe appropriato per questo; si tratta di dati di sola lettura a cui tutti devono accedere. Non ha senso passarlo tra i processi; nel peggiore dei casi ognuno può leggere la propria copia. Sto tentando di salvare la memoria non avendo una copia separata in ogni processo.
Parand

È possibile disporre di un processo master che delega parti di dati su cui lavorare agli altri processi slave. O gli schiavi possono chiedere dati o possono inviare dati. In questo modo non tutti i processi avranno una copia dell'intero oggetto.
Vasil

1
@Vasil: cosa succede se ogni processo necessita dell'intero set di dati e sta solo eseguendo un'operazione diversa su di esso?
Il

1

Non direttamente correlato al multiprocessing di per sé, ma dal tuo esempio, sembrerebbe che potresti semplicemente usare il modulo shelve o qualcosa del genere. "Big_lookup_object" deve davvero essere completamente in memoria?


Buon punto, non ho confrontato direttamente le prestazioni di su disco con quelle in memoria. Avevo pensato che ci sarebbe stata una grande differenza, ma in realtà non l'ho testato.
Parand

1

No, ma puoi caricare i tuoi dati come processo figlio e consentirgli di condividere i suoi dati con altri bambini. vedi sotto.

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)    

-4

Per la piattaforma Linux / Unix / MacOS, forkmap è una soluzione rapida e sporca.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.