web-dev-qa-db-fra.com

Python Multiprocessing.Pool paresseux itération

Je me demande comment la classe Multiprocessing.Pool de python fonctionne avec map, imap et map_async. Mon problème particulier est que je souhaite mapper sur un itérateur qui crée des objets gourmands en mémoire et que je ne souhaite pas que tous ces objets soient générés en mémoire en même temps. Je voulais voir si les diverses fonctions map () essoreraient mon itérateur, ou appelleraient intelligemment la fonction next () uniquement lorsque les processus enfants progressaient lentement, alors j'ai piraté certains tests en tant que tels:

def g():
  for el in xrange(100):
    print el
    yield el

def f(x):
  time.sleep(1)
  return x*x

if __name__ == '__main__':
  pool = Pool(processes=4)              # start 4 worker processes
  go = g()
  g2 = pool.imap(f, go)
  g2.next()

Et ainsi de suite avec map, imap et map_async. C'est l'exemple le plus flagrant cependant, car simplement appeler next () une seule fois sur g2 affiche tous mes éléments de mon générateur g (), alors que si imap faisait cela paresseusement, je m'attendrais à ce qu'il n'appelle que go.next () une fois, et donc n'imprimez que "1".

Quelqu'un peut-il clarifier ce qui se passe et s'il existe un moyen pour que le pool de processus évalue "paresseusement" l'itérateur au besoin?

Merci,

Gabe

60
Gabe

Regardons d'abord la fin du programme.

Le module de multitraitement utilise atexit pour appeler multiprocessing.util._exit_function À la fin de votre programme.

Si vous supprimez g2.next(), votre programme se termine rapidement.

_exit_function Appelle finalement Pool._terminate_pool. Le thread principal change l'état de pool._task_handler._state De RUN à TERMINATE. Pendant ce temps, le thread pool._task_handler Fait une boucle dans Pool._handle_tasks Et s'interrompt lorsqu'il atteint la condition

            if thread._state:
                debug('task handler found thread._state != RUN')
                break

(Voir /usr/lib/python2.6/multiprocessing/pool.py)

C'est ce qui empêche le gestionnaire de tâches de consommer complètement votre générateur, g(). Si vous regardez dans Pool._handle_tasks Vous verrez

        for i, task in enumerate(taskseq):
            ...
            try:
                put(task)
            except IOError:
                debug('could not put task on queue')
                break

C'est le code qui consomme votre générateur. (taskseq n'est pas exactement votre générateur, mais comme taskseq est consommé, votre générateur l'est aussi.)

En revanche, lorsque vous appelez g2.next(), le thread principal appelle IMapIterator.next Et attend lorsqu'il atteint self._cond.wait(timeout).

Le fait que le thread principal attend au lieu d'appeler _exit_function Permet au thread du gestionnaire de tâches de s'exécuter normalement, ce qui signifie consommer entièrement le générateur car il puts tâches dans le workers ' inqueue dans la fonction Pool._handle_tasks.

L'essentiel est que toutes les fonctions de carte Pool consomment l'intégralité de l'itérable qui lui est donné. Si vous souhaitez consommer le générateur en morceaux, vous pouvez le faire à la place:

import multiprocessing as mp
import itertools
import time


def g():
    for el in xrange(50):
        print el
        yield el


def f(x):
    time.sleep(1)
    return x * x

if __== '__main__':
    pool = mp.Pool(processes=4)              # start 4 worker processes
    go = g()
    result = []
    N = 11
    while True:
        g2 = pool.map(f, itertools.islice(go, N))
        if g2:
            result.extend(g2)
            time.sleep(1)
        else:
            break
    print(result)
33
unutbu

J'ai aussi eu ce problème et j'ai été déçu d'apprendre que la carte consomme tous ses éléments. J'ai codé une fonction qui consomme l'itérateur paresseusement en utilisant le type de données Queue en multitraitement. Ceci est similaire à ce que @unutbu décrit dans un commentaire à sa réponse, mais comme il le souligne, souffre de l'absence de mécanisme de rappel pour recharger la file d'attente. Le type de données Queue expose à la place un paramètre de délai d'attente et j'ai utilisé 100 millisecondes à bon escient.

from multiprocessing import Process, Queue, cpu_count
from Queue import Full as QueueFull
from Queue import Empty as QueueEmpty

def worker(recvq, sendq):
    for func, args in iter(recvq.get, None):
        result = func(*args)
        sendq.put(result)

def pool_imap_unordered(function, iterable, procs=cpu_count()):
    # Create queues for sending/receiving items from iterable.

    sendq = Queue(procs)
    recvq = Queue()

    # Start worker processes.

    for rpt in xrange(procs):
        Process(target=worker, args=(sendq, recvq)).start()

    # Iterate iterable and communicate with worker processes.

    send_len = 0
    recv_len = 0
    itr = iter(iterable)

    try:
        value = itr.next()
        while True:
            try:
                sendq.put((function, value), True, 0.1)
                send_len += 1
                value = itr.next()
            except QueueFull:
                while True:
                    try:
                        result = recvq.get(False)
                        recv_len += 1
                        yield result
                    except QueueEmpty:
                        break
    except StopIteration:
        pass

    # Collect all remaining results.

    while recv_len < send_len:
        result = recvq.get()
        recv_len += 1
        yield result

    # Terminate worker processes.

    for rpt in xrange(procs):
        sendq.put(None)

Cette solution a l'avantage de ne pas grouper les requêtes vers Pool.map. Un travailleur individuel ne peut pas empêcher les autres de progresser. YMMV. Notez que vous souhaiterez peut-être utiliser un objet différent pour signaler la fin des travaux aux travailleurs. Dans l'exemple, j'ai utilisé None.

Testé sur "Python 2.7 (r27: 82525, 4 juillet 2010, 09:01:59) [MSC v.1500 32 bits (Intel)] sur win32"

4
GrantJ

Ce que vous voulez est implémenté dans le package NuMap , à partir du site Web:

NuMap est un remplacement de fonction parallèle (basé sur un thread ou un processus, local ou distant), tampon, multi-tâches, itertools.imap ou multiprocessing.Pool.imap. Comme imap, il évalue une fonction sur des éléments d'une séquence ou itérable, et il le fait paresseusement. La paresse peut être ajustée via les arguments "stride" et "buffer".

4
letmaik