Multiprocessing - Pipe vs Queue


Risposte:


281
  • A Pipe()può avere solo due endpoint.

  • A Queue()può avere più produttori e consumatori.

Quando usarli

Se sono necessari più di due punti per comunicare, utilizzare a Queue().

Se hai bisogno di prestazioni assolute, a Pipe()è molto più veloce perché Queue()è costruito sopra Pipe().

Benchmarking delle prestazioni

Supponiamo che tu voglia generare due processi e inviare messaggi tra loro il più rapidamente possibile. Questi sono i risultati di cronometraggio di una gara di resistenza tra test simili usando Pipe()e Queue()... Questo è su un ThinkpadT61 con Ubuntu 11.10 e Python 2.7.2.

Cordiali saluti, ho lanciato i risultati JoinableQueue()come bonus; JoinableQueue()tiene conto delle attività quando queue.task_done()viene chiamato (non conosce nemmeno l'attività specifica, conta solo le attività non completate nella coda), in modo che queue.join()sappia che il lavoro è finito.

Il codice per ciascuno in fondo a questa risposta ...

mpenning@mpenning-T61:~$ python multi_pipe.py 
Sending 10000 numbers to Pipe() took 0.0369849205017 seconds
Sending 100000 numbers to Pipe() took 0.328398942947 seconds
Sending 1000000 numbers to Pipe() took 3.17266988754 seconds
mpenning@mpenning-T61:~$ python multi_queue.py 
Sending 10000 numbers to Queue() took 0.105256080627 seconds
Sending 100000 numbers to Queue() took 0.980564117432 seconds
Sending 1000000 numbers to Queue() took 10.1611330509 seconds
mpnening@mpenning-T61:~$ python multi_joinablequeue.py 
Sending 10000 numbers to JoinableQueue() took 0.172781944275 seconds
Sending 100000 numbers to JoinableQueue() took 1.5714070797 seconds
Sending 1000000 numbers to JoinableQueue() took 15.8527247906 seconds
mpenning@mpenning-T61:~$

In sintesi Pipe()è circa tre volte più veloce di a Queue(). Non pensare nemmeno a JoinableQueue()meno che tu non debba davvero avere i benefici.

MATERIALE BONUS 2

Il multiprocessing introduce sottili cambiamenti nel flusso di informazioni che rendono difficile il debug a meno che non si conoscano alcune scorciatoie. Ad esempio, potresti avere uno script che funziona bene quando si indicizza un dizionario in molte condizioni, ma raramente fallisce con determinati input.

Normalmente otteniamo indizi sull'errore quando l'intero processo di Python si arresta in modo anomalo; tuttavia, non si ottengono tracce di crash non richieste stampate sulla console se la funzione multiprocessore si arresta in modo anomalo. È difficile rintracciare gli arresti anomali del multiprocessing senza avere la minima idea di cosa abbia provocato l'arresto anomalo del processo.

Il modo più semplice che ho trovato per rintracciare le informazioni sull'arresto anomalo del multiprocessing è avvolgere l'intera funzione multiprocessing in un try/ excepte usare traceback.print_exc():

import traceback
def run(self, args):
    try:
        # Insert stuff to be multiprocessed here
        return args[0]['that']
    except:
        print "FATAL: reader({0}) exited while multiprocessing".format(args) 
        traceback.print_exc()

Ora, quando trovi un incidente, vedi qualcosa del tipo:

FATAL: reader([{'crash': 'this'}]) exited while multiprocessing
Traceback (most recent call last):
  File "foo.py", line 19, in __init__
    self.run(args)
  File "foo.py", line 46, in run
    KeyError: 'that'

Codice sorgente:


"""
multi_pipe.py
"""
from multiprocessing import Process, Pipe
import time

def reader_proc(pipe):
    ## Read from the pipe; this will be spawned as a separate Process
    p_output, p_input = pipe
    p_input.close()    # We are only reading
    while True:
        msg = p_output.recv()    # Read from the output pipe and do nothing
        if msg=='DONE':
            break

def writer(count, p_input):
    for ii in xrange(0, count):
        p_input.send(ii)             # Write 'count' numbers into the input pipe
    p_input.send('DONE')

if __name__=='__main__':
    for count in [10**4, 10**5, 10**6]:
        # Pipes are unidirectional with two endpoints:  p_input ------> p_output
        p_output, p_input = Pipe()  # writer() writes to p_input from _this_ process
        reader_p = Process(target=reader_proc, args=((p_output, p_input),))
        reader_p.daemon = True
        reader_p.start()     # Launch the reader process

        p_output.close()       # We no longer need this part of the Pipe()
        _start = time.time()
        writer(count, p_input) # Send a lot of stuff to reader_proc()
        p_input.close()
        reader_p.join()
        print("Sending {0} numbers to Pipe() took {1} seconds".format(count,
            (time.time() - _start)))

"""
multi_queue.py
"""

from multiprocessing import Process, Queue
import time
import sys

def reader_proc(queue):
    ## Read from the queue; this will be spawned as a separate Process
    while True:
        msg = queue.get()         # Read from the queue and do nothing
        if (msg == 'DONE'):
            break

def writer(count, queue):
    ## Write to the queue
    for ii in range(0, count):
        queue.put(ii)             # Write 'count' numbers into the queue
    queue.put('DONE')

if __name__=='__main__':
    pqueue = Queue() # writer() writes to pqueue from _this_ process
    for count in [10**4, 10**5, 10**6]:             
        ### reader_proc() reads from pqueue as a separate process
        reader_p = Process(target=reader_proc, args=((pqueue),))
        reader_p.daemon = True
        reader_p.start()        # Launch reader_proc() as a separate python process

        _start = time.time()
        writer(count, pqueue)    # Send a lot of stuff to reader()
        reader_p.join()         # Wait for the reader to finish
        print("Sending {0} numbers to Queue() took {1} seconds".format(count, 
            (time.time() - _start)))

"""
multi_joinablequeue.py
"""
from multiprocessing import Process, JoinableQueue
import time

def reader_proc(queue):
    ## Read from the queue; this will be spawned as a separate Process
    while True:
        msg = queue.get()         # Read from the queue and do nothing
        queue.task_done()

def writer(count, queue):
    for ii in xrange(0, count):
        queue.put(ii)             # Write 'count' numbers into the queue

if __name__=='__main__':
    for count in [10**4, 10**5, 10**6]:
        jqueue = JoinableQueue() # writer() writes to jqueue from _this_ process
        # reader_proc() reads from jqueue as a different process...
        reader_p = Process(target=reader_proc, args=((jqueue),))
        reader_p.daemon = True
        reader_p.start()     # Launch the reader process
        _start = time.time()
        writer(count, jqueue) # Send a lot of stuff to reader_proc() (in different process)
        jqueue.join()         # Wait for the reader to finish
        print("Sending {0} numbers to JoinableQueue() took {1} seconds".format(count, 
            (time.time() - _start)))

2
@Jonathan "In sintesi Pipe () è circa tre volte più veloce di una Queue ()"
James Brady,

13
Eccellente! Buona risposta e bello che hai fornito benchmark! Ho solo due piccoli cavilli: (1) "ordini di grandezza più veloci" è un po 'un'esagerazione. La differenza è x3, che è circa un terzo di un ordine di grandezza. Sto solo dicendo. ;-); e (2) un confronto più equo sarebbe in esecuzione di N lavoratori, ciascuno dei quali comunica con il thread principale tramite pipe point-to-point rispetto alle prestazioni di N lavoratori in esecuzione tutti che eseguono il pull da una singola coda da punto a multipunto.
JJC

3
Per il tuo "Materiale bonus" ... Sì. Se stai eseguendo la sottoclasse del processo, inserisci la maggior parte del metodo "run" in un blocco try. Questo è anche un modo utile per fare la registrazione delle eccezioni. Per replicare il normale output di eccezione: sys.stderr.write (''. Join (traceback.format_exception (* (sys.exc_info ()))))
travc,

2
@ alexpinho98 - ma avrai bisogno di alcuni dati fuori banda e della modalità di segnalazione associata, per indicare che ciò che stai inviando non sono dati regolari ma dati di errore. visto che il processo di origine è già in uno stato imprevedibile, questo potrebbe essere troppo da chiedere.
scytale,

10
@JJC Per cavillare con il tuo cavillo, 3x è circa mezzo ordine di grandezza, non un terzo - sqrt (10) = ~ 3.
jab

1

Un'altra caratteristica Queue()degna di nota è il filo dell'alimentatore. Questa sezione indica "Quando un processo inserisce per la prima volta un elemento nella coda, viene avviato un thread di alimentazione che trasferisce gli oggetti da un buffer nel pipe". È possibile inserire un numero infinito di (o dimensioni massime) di elementi Queue()senza chiamate al queue.put()blocco. Ciò consente di archiviare più elementi in un Queue(), fino a quando il programma non è pronto per elaborarli.

Pipe()d'altra parte, ha una quantità limitata di spazio di archiviazione per gli articoli che sono stati inviati a una connessione, ma che non sono stati ricevuti dall'altra connessione. Dopo aver esaurito questo spazio di archiviazione, le chiamate a connection.send()verranno bloccate fino a quando non c'è spazio per scrivere l'intero elemento. Questo bloccherà il thread facendo la scrittura fino a quando un altro thread non legge dalla pipe. Connectiongli oggetti ti danno accesso al descrittore di file sottostante. Sui sistemi * nix, è possibile impedire il connection.send()blocco delle chiamate utilizzando ilos.set_blocking() funzione. Tuttavia, ciò provocherà problemi se si tenta di inviare un singolo elemento che non si adatta al file della pipe. Le versioni recenti di Linux consentono di aumentare le dimensioni di un file, ma le dimensioni massime consentite variano in base alle configurazioni di sistema. Pertanto non dovresti mai fare affidamento sui Pipe()dati del buffer. Chiamate aconnection.send potrebbe bloccare fino a quando i dati non vengono letti dalla pipe da qualche altra parte.

In conclusione, la coda è una scelta migliore rispetto alla pipe quando è necessario bufferizzare i dati. Anche quando devi solo comunicare tra due punti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.