Python Process Pool non demoniaco?


96

Sarebbe possibile creare un pool python non demoniaco? Voglio che un pool possa chiamare una funzione che ha un altro pool all'interno.

Lo voglio perché i processi demoniaci non possono creare processi. In particolare, causerà l'errore:

AssertionError: daemonic processes are not allowed to have children

Ad esempio, si consideri lo scenario in cui function_aè presente un pool in esecuzione function_bche dispone di un pool in esecuzione function_c. Questa catena di funzioni fallirà, perché function_bviene eseguita in un processo daemon ei processi daemon non possono creare processi.


AFAIK, no, non è possibile che tutti i worker nel pool siano demonizzati e non è possibile iniettare la dipendenza , BTW non capisco la seconda parte della tua domanda I want a pool to be able to call a function that has another pool insidee come ciò interferisca con il fatto che i worker siano demonizzati.
mouad

4
Perché se la funzione a ha un pool che esegue la funzione b che ha un pool che esegue la funzione c, c'è un problema in b che è in esecuzione in un processo daemon, ei processi daemon non possono creare processi. AssertionError: daemonic processes are not allowed to have children
Max

Risposte:


118

La multiprocessing.pool.Poolclasse crea i processi di lavoro nel suo __init__metodo, li rende demoniaci e li avvia, e non è possibile reimpostare il loro daemonattributo a Falseprima che siano avviati (e successivamente non è più consentito). Ma puoi creare la tua sottoclasse di multiprocesing.pool.Pool( multiprocessing.Poolè solo una funzione wrapper) e sostituire la tua multiprocessing.Processsottoclasse, che è sempre non demoniaca, da utilizzare per i processi di lavoro.

Ecco un esempio completo di come farlo. Le parti importanti sono le due classi NoDaemonProcesse MyPoolin alto e da chiamare pool.close()e pool.join()sulla tua MyPoolistanza alla fine.

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import multiprocessing
# We must import this explicitly, it is not imported by the top-level
# multiprocessing module.
import multiprocessing.pool
import time

from random import randint


class NoDaemonProcess(multiprocessing.Process):
    # make 'daemon' attribute always return False
    def _get_daemon(self):
        return False
    def _set_daemon(self, value):
        pass
    daemon = property(_get_daemon, _set_daemon)

# We sub-class multiprocessing.pool.Pool instead of multiprocessing.Pool
# because the latter is only a wrapper function, not a proper class.
class MyPool(multiprocessing.pool.Pool):
    Process = NoDaemonProcess

def sleepawhile(t):
    print("Sleeping %i seconds..." % t)
    time.sleep(t)
    return t

def work(num_procs):
    print("Creating %i (daemon) workers and jobs in child." % num_procs)
    pool = multiprocessing.Pool(num_procs)

    result = pool.map(sleepawhile,
        [randint(1, 5) for x in range(num_procs)])

    # The following is not really needed, since the (daemon) workers of the
    # child's pool are killed when the child is terminated, but it's good
    # practice to cleanup after ourselves anyway.
    pool.close()
    pool.join()
    return result

def test():
    print("Creating 5 (non-daemon) workers and jobs in main process.")
    pool = MyPool(5)

    result = pool.map(work, [randint(1, 5) for x in range(5)])

    pool.close()
    pool.join()
    print(result)

if __name__ == '__main__':
    test()

1
Ho appena testato di nuovo il mio codice con Python 2.7 / 3.2 (dopo aver corretto le linee di "stampa") su Linux e Python 2.6 / 2.7 / 3.2 OS X. Linux e Python 2.7 / 3.2 su OS X funzionano bene ma il codice è effettivamente bloccato con Python 2.6 su OS X (Lion). Questo sembra essere un bug nel modulo multiprocessing, che è stato corretto, ma in realtà non ho controllato il bug tracker.
Chris Arndt,

1
Grazie! Su Windows devi anche chiamaremultiprocessing.freeze_support()
frmdstryr

2
Bel lavoro. Se qualcuno sta riscontrando una perdita di memoria con questo, prova a utilizzare "con chiusura (MyPool (processi = num_cpu)) come pool:" per smaltire correttamente il pool
Chris Lucian

31
Quali sono gli svantaggi dell'utilizzo al MyPoolposto del valore predefinito Pool? In altre parole, in cambio della flessibilità di avviare processi figlio, quali costi devo pagare? (Se non ci fossero stati costi, presumibilmente lo standard Poolavrebbe utilizzato processi non demoniaci).
max

4
@machen Sì, purtroppo è vero. In Python 3.6 la Poolclasse è stata ampiamente rifattorizzata, quindi Processnon è più un semplice attributo, ma un metodo, che restituisce l'istanza di processo che ottiene da un contesto . Ho provato a sovrascrivere questo metodo per restituire NoDaemonPoolun'istanza, ma questo si traduce in un'eccezione AssertionError: daemonic processes are not allowed to have childrenquando viene utilizzato il pool.
Chris Arndt

26

Ho avuto la necessità di utilizzare un pool non demoniaco in Python 3.7 e ho finito per adattare il codice pubblicato nella risposta accettata. Di seguito c'è lo snippet che crea il pool non demoniaco:

class NoDaemonProcess(multiprocessing.Process):
    @property
    def daemon(self):
        return False

    @daemon.setter
    def daemon(self, value):
        pass


class NoDaemonContext(type(multiprocessing.get_context())):
    Process = NoDaemonProcess

# We sub-class multiprocessing.pool.Pool instead of multiprocessing.Pool
# because the latter is only a wrapper function, not a proper class.
class MyPool(multiprocessing.pool.Pool):
    def __init__(self, *args, **kwargs):
        kwargs['context'] = NoDaemonContext()
        super(MyPool, self).__init__(*args, **kwargs)

Poiché l'attuale implementazione di multiprocessingè stata ampiamente riformulata per essere basata sui contesti, è necessario fornire una NoDaemonContextclasse che abbia il nostro NoDaemonProcessattributo as.MyPoolutilizzerà quindi quel contesto invece di quello predefinito.

Detto questo, devo avvertire che ci sono almeno 2 avvertimenti a questo approccio:

  1. Dipende ancora dai dettagli di implementazione di multiprocessing pacchetto e potrebbe quindi interrompersi in qualsiasi momento.
  2. Ci sono valide ragioni per cui multiprocessingè stato così difficile usare processi non demoniaci, molti dei quali sono spiegati qui . Il più convincente secondo me è:

    Per quanto riguarda il consentire ai thread figli di generare figli da soli usando il sottoprocesso, si corre il rischio di creare un piccolo esercito di "nipoti" zombi se i thread padre o figlio terminano prima che il sottoprocesso venga completato e ritorni.


Per quanto riguarda l'avvertenza: il mio caso d'uso consiste nel parallelizzare le attività, ma i nipoti restituiscono le informazioni ai loro genitori che a loro volta restituiscono le informazioni ai loro genitori dopo aver eseguito alcune elaborazioni locali richieste. Di conseguenza, ogni livello / ramo ha un'attesa esplicita per tutte le sue foglie. L'avvertenza si applica ancora se devi aspettare esplicitamente che i processi generati finiscano?
A_A

Ottenere l'errore AttributeError: module 'multiprocessing' has no attribute 'pool'in Python 3.8.0
Nyxynyx

@Nyxynyx Non dimenticareimport multiprocessing.pool
Chris Arndt

22

Il modulo multiprocessing ha una bella interfaccia per usare pool con processi o thread. A seconda del tuo caso d'uso corrente, potresti prendere in considerazione l'utilizzo multiprocessing.pool.ThreadPoolper il tuo Pool esterno, che si tradurrà in thread (che consentono di generare processi dall'interno) anziché processi.

Potrebbe essere limitato dal GIL, ma nel mio caso particolare (ho testato entrambi) , il tempo di avvio per i processi dall'esterno Poolcome creato qui ha superato di gran lunga la soluzione con ThreadPool.


E 'davvero facile da scambiare Processesper Threads. Leggi di più su come utilizzare una ThreadPoolsoluzione qui o qui .


Grazie - questo mi ha aiutato molto - ottimo uso del threading qui (per generare processi che funzionano effettivamente bene)
trance_dude

1
Per le persone che cercano una soluzione pratica che probabilmente si applichi alla loro situazione, questa è quella giusta.
abanana

6

Su alcune versioni di Python sostituzione standard di piscina a personalizzato può sollevare errore: AssertionError: group argument must be None for now.

Qui ho trovato una soluzione che può aiutare:

class NoDaemonProcess(multiprocessing.Process):
    # make 'daemon' attribute always return False
    @property
    def daemon(self):
        return False

    @daemon.setter
    def daemon(self, val):
        pass


class NoDaemonProcessPool(multiprocessing.pool.Pool):

    def Process(self, *args, **kwds):
        proc = super(NoDaemonProcessPool, self).Process(*args, **kwds)
        proc.__class__ = NoDaemonProcess

        return proc

4

concurrent.futures.ProcessPoolExecutornon ha questa limitazione. Può avere un pool di processi annidato senza alcun problema:

from concurrent.futures import ProcessPoolExecutor as Pool
from itertools import repeat
from multiprocessing import current_process
import time

def pid():
    return current_process().pid

def _square(i):  # Runs in inner_pool
    square = i ** 2
    time.sleep(i / 10)
    print(f'{pid()=} {i=} {square=}')
    return square

def _sum_squares(i, j):  # Runs in outer_pool
    with Pool(max_workers=2) as inner_pool:
        squares = inner_pool.map(_square, (i, j))
    sum_squares = sum(squares)
    time.sleep(sum_squares ** .5)
    print(f'{pid()=}, {i=}, {j=} {sum_squares=}')
    return sum_squares

def main():
    with Pool(max_workers=3) as outer_pool:
        for sum_squares in outer_pool.map(_sum_squares, range(5), repeat(3)):
            print(f'{pid()=} {sum_squares=}')

if __name__ == "__main__":
    main()

Il codice dimostrativo sopra è stato testato con Python 3.8.

Credito: risposta di jfs


1
Questa è ora chiaramente la soluzione migliore, poiché richiede modifiche minime.
DreamFlasher

1
funziona perfettamente! ... come nota a margine è anche possibile usare un bambino multiprocessing.Pooldentro a ProcessPoolExecutor.Pool!
raphael

3

Il problema che ho riscontrato era nel tentativo di importare le globali tra i moduli, facendo sì che la riga ProcessPool () venisse valutata più volte.

globals.py

from processing             import Manager, Lock
from pathos.multiprocessing import ProcessPool
from pathos.threading       import ThreadPool

class SingletonMeta(type):
    def __new__(cls, name, bases, dict):
        dict['__deepcopy__'] = dict['__copy__'] = lambda self, *args: self
        return super(SingletonMeta, cls).__new__(cls, name, bases, dict)

    def __init__(cls, name, bases, dict):
        super(SingletonMeta, cls).__init__(name, bases, dict)
        cls.instance = None

    def __call__(cls,*args,**kw):
        if cls.instance is None:
            cls.instance = super(SingletonMeta, cls).__call__(*args, **kw)
        return cls.instance

    def __deepcopy__(self, item):
        return item.__class__.instance

class Globals(object):
    __metaclass__ = SingletonMeta
    """     
    This class is a workaround to the bug: AssertionError: daemonic processes are not allowed to have children

    The root cause is that importing this file from different modules causes this file to be reevalutated each time, 
    thus ProcessPool() gets reexecuted inside that child thread, thus causing the daemonic processes bug    
    """
    def __init__(self):
        print "%s::__init__()" % (self.__class__.__name__)
        self.shared_manager      = Manager()
        self.shared_process_pool = ProcessPool()
        self.shared_thread_pool  = ThreadPool()
        self.shared_lock         = Lock()        # BUG: Windows: global name 'lock' is not defined | doesn't affect cygwin

Quindi importa in modo sicuro da altre parti del codice

from globals import Globals
Globals().shared_manager      
Globals().shared_process_pool
Globals().shared_thread_pool  
Globals().shared_lock         

2

Ho visto persone che si occupano di questo problema usando celeryil fork di multiprocessingchiamato billiard (estensioni di pool multiprocessing), che consente ai processi demonici di generare bambini. La procedura dettagliata consiste nel sostituire semplicemente il multiprocessingmodulo con:

import billiard as multiprocessing
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.