multiprocessing in python: cosa viene ereditato dal processo forkserver dal processo genitore?

Aug 15 2020

Sto cercando di utilizzare forkservere ho incontrato NameError: name 'xxx' is not definednei processi di lavoro.

Sto usando Python 3.6.4, ma la documentazione dovrebbe essere la stessa, dahttps://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodsdice che:

Il processo server fork è a thread singolo, quindi è sicuro che utilizzi os.fork(). Nessuna risorsa non necessaria viene ereditata.

Inoltre, dice:

Meglio ereditare che mettere in salamoia/non mettere in salamoia

Quando si utilizzano i metodi di avvio spawn o forkserver , molti tipi di multiprocessing devono essere selezionabili in modo che i processi figli possano utilizzarli . Tuttavia, si dovrebbe generalmente evitare di inviare oggetti condivisi ad altri processi utilizzando pipe o code. Invece dovresti organizzare il programma in modo che un processo che ha bisogno di accedere a una risorsa condivisa creata altrove possa ereditarlo da un processo antenato.

Quindi apparentemente un oggetto chiave su cui il mio processo di lavoro deve lavorare non è stato ereditato dal processo del server e quindi passato ai lavoratori, perché è successo? Mi chiedo cosa venga ereditato esattamente dal processo forkserver dal processo genitore?

Ecco come appare il mio codice:

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)

Grazie!

Migliore,

Risposte

1 alex_noname Aug 16 2020 at 19:32

Teoria

Di seguito è riportato un estratto dal blog di Bojan Nikolic

Le moderne versioni di Python (su Linux) forniscono tre modi per avviare i processi separati:

  1. Fork() -ing i processi padre e continuando con la stessa immagine dei processi sia in padre che in figlio. Questo metodo è veloce, ma potenzialmente inaffidabile quando lo stato padre è complesso

  2. Generazione dei processi figli, ad esempio fork()-ing e quindi execv per sostituire l'immagine del processo con un nuovo processo Python. Questo metodo è affidabile ma lento, poiché l'immagine del processo viene ricaricata di nuovo.

  3. Il meccanismo forkserver , che consiste in un server Python separato con uno stato relativamente semplice e che è fork() quando è necessario un nuovo processo. Questo metodo combina la velocità di Fork() con una buona affidabilità (poiché il genitore sottoposto a fork è in uno stato semplice).

Forkserver

Il terzo metodo, forkserver , è illustrato di seguito. Si noti che i figli conservano una copia dello stato forkserver. Questo stato dovrebbe essere relativamente semplice, ma è possibile regolarlo tramite l'API multiprocesso tramite il set_forkserver_preload()metodo.

Pratica

Pertanto, se si desidera che qualcosa venga ereditato dai processi figlio dal genitore, questo deve essere specificato nello stato forkserver tramite set_forkserver_preload(modules_names), che imposta l'elenco dei nomi dei moduli da provare a caricare nel processo forkserver. Faccio un esempio qui sotto:

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

Produzione:

# 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 non usiamo il precaricamento

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

Quindi, dopo una stimolante discussione con Alex, penso di avere informazioni sufficienti per rispondere alla mia domanda: cosa viene ereditato esattamente dal processo forkserver dal processo genitore?

Fondamentalmente quando il processo del server si avvia, importerà il tuo modulo principale e tutto ciò if __name__ == '__main__'che verrà eseguito prima. Ecco perché il mio codice non funziona, perché large_objectnon si trova da nessuna parte in serverprocess e in tutti quei processi worker che si diramano da serverprocess .

La soluzione di Alex funziona perché large_objectora viene importata sia nel processo principale che in quello del server, quindi anche ogni lavoratore biforcuto dal server riceverà large_object. Se combinato con set_forkserver_preload(modules_names)tutti i lavoratori potrebbe anche ottenere lo stesso large_object da quello che ho visto. Il motivo dell'utilizzo forkserverè spiegato esplicitamente nelle documentazioni di Python e nel blog di Bojan:

Quando il programma si avvia e seleziona il metodo di avvio forkserver, viene avviato un processo del server. Da quel momento in poi, ogni volta che è necessario un nuovo processo, il processo padre si connette al server e gli richiede di eseguire il fork di un nuovo processo. Il processo server fork è a thread singolo, quindi è sicuro che utilizzi os.fork(). Nessuna risorsa non necessaria viene ereditata .

Il meccanismo forkserver, che consiste in un server Python separato con uno stato relativamente semplice e che è fork() quando è necessario un nuovo processo. Questo metodo combina la velocità di Fork()-ing con una buona affidabilità (poiché il genitore che viene biforcato è in uno stato semplice) .

Quindi è più sul sicuro della preoccupazione qui.

In una nota a margine, se si utilizza forkcome metodo di partenza, tuttavia, non è necessario importare nulla poiché tutti i processi figlio ottengono una copia della memoria del processo genitori (o un riferimento se il sistema utilizza COW- copy-on-write, per favore correggimi se lo sono sbagliato). In questo caso l'utilizzo global large_objectti consentirà di accedere direttamente a large_objectin worker_func.

forkserverPotrebbe non essere un approccio adatto a me perché il problema che sto affrontando è il sovraccarico della memoria . Tutte le operazioni che mi portano large_objectin primo luogo consumano memoria, quindi non voglio risorse non necessarie nei miei processi di lavoro.

Se inserisco tutti quei calcoli direttamente inherited.pycome suggerito da Alex, verrà eseguito due volte (una volta quando ho importato il modulo in main e una volta quando il server lo importa; forse anche di più quando sono nati i processi di lavoro?), questo è adatto se io voglio solo un processo sicuro a thread singolo da cui i lavoratori possano eseguire il fork. Ma dal momento che sto cercando di convincere i lavoratori a non ereditare risorse non necessarie e ottenere solo large_object, questo non funzionerà. __main__E anche l'inserimento di questi calcoli inherited.pynon funzionerà poiché ora nessuno dei processi li eseguirà, inclusi main e server.

Quindi, in conclusione, se l'obiettivo qui è far sì che i lavoratori ereditino risorse minime, è meglio suddividere il mio codice in 2, fare calculation.pyprima pickle the large_object, uscire dall'interprete e avviarne uno nuovo per caricare pickled large_object. Quindi posso semplicemente impazzire con uno forko forkserver.