web-dev-qa-db-fra.com

Qui exécute le rappel lors de l'utilisation de la méthode apply_async d'un pool de multitraitement?

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

37
Alex

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.

36
dano