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
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 put
s tâches dans le worker
s ' 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)
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"
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".