J'essaie de comprendre un peu ce qui se passe dans les coulisses lors de l'utilisation de la méthode apply_sync d'un pool de multitraitement.
Qui exécute la méthode de rappel? Est-ce le processus principal qui a appelé apply_async?
Supposons que j'envoie un tas de commandes apply_async avec des rappels, puis je continue mon programme. Mon programme fait toujours des choses lorsque le début de apply_async se termine. Comment le rappel s'exécute-t-il dans le "processus principal" alors que le processus principal est toujours occupé par le script?
Voici un exemple.
import multiprocessing
import time
def callback(x):
print '{} running callback with arg {}'.format(multiprocessing.current_process().name, x)
def func(x):
print '{} running func with arg {}'.format(multiprocessing.current_process().name, x)
return x
pool = multiprocessing.Pool()
args = range(20)
for a in args:
pool.apply_async(func, (a,), callback=callback)
print '{} going to sleep for a minute'.format(multiprocessing.current_process().name)
t0 = time.time()
while time.time() - t0 < 60:
pass
print 'Finished with the script'
La sortie est quelque chose comme
PoolWorker-1 exécutant func avec arg 0
PoolWorker-2 exécutant func avec arg 1
PoolWorker-3 exécutant func avec arg 2
Processus principal en veille pendant une minute <- le processus principal est occupé
PoolWorker-4 exécutant func avec arg 3
PoolWorker-1 exécutant func avec arg 4
PoolWorker-2 exécutant func avec arg 5
PoolWorker-3 exécutant func avec arg 6
PoolWorker-4 exécutant func avec arg 7
MainProcess exécutant le rappel avec arg 0 <- processus principal exécutant le rappel alors qu'il est encore dans la boucle while !!
MainProcess exécutant le rappel avec arg 1
MainProcess exécutant un rappel avec arg 2
MainProcess exécutant un rappel avec arg 3
MainProcess exécutant un rappel avec arg 4
PoolWorker-1 exécutant func avec arg 8
...
Terminé avec le script
Comment MainProcess exécute-t-il le rappel alors qu'il est au milieu de cette boucle while?
Il y a cette déclaration sur le rappel dans la documentation de multiprocessing.Pool qui semble être un indice mais je ne la comprends pas.
apply_async (func [ args [ kwds [ callback]]])
Une variante de la méthode apply () qui retourne un objet résultat.
Si le rappel est spécifié, il doit s'agir d'un appelable qui accepte un seul argument. Lorsque le résultat est prêt, le rappel lui est appliqué (sauf si l'appel a échoué). le rappel devrait se terminer immédiatement car sinon le thread qui gère les résultats sera bloqué.
Il y a en effet un indice dans les documents:
le rappel devrait se terminer immédiatement puisque sinon le thread qui gère les résultats sera bloqué.
Les rappels sont gérés dans le processus principal, mais ils sont exécutés dans leur propre thread séparé . Lorsque vous créez un Pool
, il crée en fait quelques Thread
objets en interne:
class Pool(object):
Process = Process
def __init__(self, processes=None, initializer=None, initargs=(),
maxtasksperchild=None):
self._setup_queues()
self._taskqueue = Queue.Queue()
self._cache = {}
... # stuff we don't care about
self._worker_handler = threading.Thread(
target=Pool._handle_workers,
args=(self, )
)
self._worker_handler.daemon = True
self._worker_handler._state = RUN
self._worker_handler.start()
self._task_handler = threading.Thread(
target=Pool._handle_tasks,
args=(self._taskqueue, self._quick_put, self._outqueue,
self._pool, self._cache)
)
self._task_handler.daemon = True
self._task_handler._state = RUN
self._task_handler.start()
self._result_handler = threading.Thread(
target=Pool._handle_results,
args=(self._outqueue, self._quick_get, self._cache)
)
self._result_handler.daemon = True
self._result_handler._state = RUN
self._result_handler.start()
Le fil intéressant pour nous est _result_handler
; nous verrons pourquoi bientôt.
Changement de vitesse pendant une seconde, lorsque vous exécutez apply_async
, il crée un objet ApplyResult
en interne pour gérer l'obtention du résultat de l'enfant:
def apply_async(self, func, args=(), kwds={}, callback=None):
assert self._state == RUN
result = ApplyResult(self._cache, callback)
self._taskqueue.put(([(result._job, None, func, args, kwds)], None))
return result
class ApplyResult(object):
def __init__(self, cache, callback):
self._cond = threading.Condition(threading.Lock())
self._job = job_counter.next()
self._cache = cache
self._ready = False
self._callback = callback
cache[self._job] = self
def _set(self, i, obj):
self._success, self._value = obj
if self._callback and self._success:
self._callback(self._value)
self._cond.acquire()
try:
self._ready = True
self._cond.notify()
finally:
self._cond.release()
del self._cache[self._job]
Comme vous pouvez le voir, le _set
est la méthode qui finit par réellement exécuter le callback
passé, en supposant que la tâche a réussi. Notez également qu'il s'ajoute à un dict global cache
à la fin de __init__
.
Maintenant, revenons au _result_handler
objet fil. Cet objet appelle le _handle_results
fonction, qui ressemble à ceci:
while 1:
try:
task = get()
except (IOError, EOFError):
debug('result handler got EOFError/IOError -- exiting')
return
if thread._state:
assert thread._state == TERMINATE
debug('result handler found thread._state=TERMINATE')
break
if task is None:
debug('result handler got sentinel')
break
job, i, obj = task
try:
cache[job]._set(i, obj) # Here is _set (and therefore our callback) being called!
except KeyError:
pass
# More stuff
C'est une boucle qui extrait simplement les résultats des enfants de la file d'attente, trouve l'entrée correspondante dans cache
et appelle _set
, qui exécute notre rappel. Il est capable de s'exécuter même si vous êtes dans une boucle car il ne s'exécute pas dans le thread principal.