Hay otro atajo que puede usar, aunque puede ser ineficiente dependiendo de lo que haya en las instancias de su clase.
Como todos han dicho, el problema es que el multiprocessing
código tiene que seleccionar las cosas que envía a los subprocesos que ha iniciado, y el selector no hace métodos de instancia.
Sin embargo, en lugar de enviar el método de instancia, puede enviar la instancia de clase real, más el nombre de la función a llamar, a una función ordinaria que luego usa getattr
para llamar al método de instancia, creando así el método enlazado en el Pool
subproceso. Esto es similar a definir un __call__
método, excepto que puede llamar a más de una función miembro.
Robando el código de @ EricH. De su respuesta y anotándolo un poco (lo reescribí, por lo tanto, todos los cambios de nombre y demás, por alguna razón, esto parecía más fácil que cortar y pegar :-)) para ilustrar toda la magia:
import multiprocessing
import os
def call_it(instance, name, args=(), kwargs=None):
"indirect caller for instance methods and multiprocessing"
if kwargs is None:
kwargs = {}
return getattr(instance, name)(*args, **kwargs)
class Klass(object):
def __init__(self, nobj, workers=multiprocessing.cpu_count()):
print "Constructor (in pid=%d)..." % os.getpid()
self.count = 1
pool = multiprocessing.Pool(processes = workers)
async_results = [pool.apply_async(call_it,
args = (self, 'process_obj', (i,))) for i in range(nobj)]
pool.close()
map(multiprocessing.pool.ApplyResult.wait, async_results)
lst_results = [r.get() for r in async_results]
print lst_results
def __del__(self):
self.count -= 1
print "... Destructor (in pid=%d) count=%d" % (os.getpid(), self.count)
def process_obj(self, index):
print "object %d" % index
return "results"
Klass(nobj=8, workers=3)
El resultado muestra que, de hecho, el constructor se llama una vez (en el pid original) y el destructor se llama 9 veces (una vez por cada copia realizada = 2 o 3 veces por proceso de grupo-trabajador según sea necesario, más una vez en el original proceso). Esto a menudo está bien, como en este caso, ya que el selector predeterminado hace una copia de toda la instancia y la rellena (semi) en secreto, en este caso, haciendo:
obj = object.__new__(Klass)
obj.__dict__.update({'count':1})
Por eso, aunque el destructor se llama ocho veces en los tres procesos de trabajo, cuenta de 1 a 0 cada vez, pero, por supuesto, aún puede meterse en problemas de esta manera. Si es necesario, puede proporcionar su propio __setstate__
:
def __setstate__(self, adict):
self.count = adict['count']
en este caso por ejemplo.
PicklingError: Can't pickle <class 'function'>: attribute lookup builtins.function failed