Multiprocessamento - Pipe vs Fila

Respostas:

281
  • A Pipe()pode ter apenas dois pontos de extremidade.

  • A Queue()pode ter vários produtores e consumidores.

Quando usá-los

Se você precisar de mais de dois pontos para se comunicar, use a Queue().

Se você precisar de desempenho absoluto, o a Pipe()é muito mais rápido, porque Queue()é construído sobre ele Pipe().

Benchmarking de desempenho

Vamos supor que você deseja gerar dois processos e enviar mensagens entre eles o mais rápido possível. Estes são os resultados de tempo de uma corrida de arrancada entre testes semelhantes usando Pipe()e Queue()... Isso ocorre em um ThinkpadT61 executando o Ubuntu 11.10 e o Python 2.7.2.

Para sua informação, joguei resultados JoinableQueue()como bônus; JoinableQueue()contabiliza tarefas quando queue.task_done()é chamado (nem sequer sabe sobre a tarefa específica, apenas conta tarefas inacabadas na fila), para que queue.join()saiba que o trabalho foi concluído.

O código para cada um na parte inferior desta resposta ...

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:~$

Em resumo, Pipe()é cerca de três vezes mais rápido que a Queue(). Nem pense nisso, a JoinableQueue()menos que você realmente precise dos benefícios.

MATERIAL DE BÔNUS 2

O multiprocessamento introduz alterações sutis no fluxo de informações que dificultam a depuração, a menos que você conheça alguns atalhos. Por exemplo, você pode ter um script que funcione bem ao indexar através de um dicionário em muitas condições, mas que raramente falhe com determinadas entradas.

Normalmente, temos pistas sobre a falha quando o processo python inteiro falha; no entanto, você não imprime rastreios de falhas não solicitados no console se a função de multiprocessamento falhar. Rastrear falhas de multiprocessamento desconhecidas é difícil, sem a menor idéia do que causou uma falha no processo.

A maneira mais simples que encontrei para rastrear informações de falhas de multiprocessamento é agrupar toda a função de multiprocessamento em um try/ excepte usar 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()

Agora, quando você encontra uma falha, vê algo como:

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'

Código fonte:


"""
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)))
Mike Pennington
fonte
2
@Jonathan "Em resumo, Pipe () é cerca de três vezes mais rápido que um Queue ()"
James Brady
13
Excelente! Boa resposta e bom que você forneceu referências! Eu só tenho duas pequenas queixas: (1) "ordens de magnitude mais rápidas" é um exagero. A diferença é x3, que é cerca de um terço de uma ordem de magnitude. Apenas dizendo. ;-); e (2) uma comparação mais justa seria executar N trabalhadores, cada um se comunicando com o encadeamento principal por meio de canal ponto a ponto em comparação com o desempenho de executar trabalhadores N, todos puxando de uma única fila ponto a multiponto.
JJC
3
Para o seu "material bônus" ... Sim. Se você estiver subclassificando Process, coloque a maior parte do método 'run' em um bloco try. Essa também é uma maneira útil de fazer o log de exceções. Para replicar a saída de exceção normal: sys.stderr.write (''. Join (traceback.format_exception (* (sys.exc_info ())))))
travc
2
@ alexpinho98 - mas você precisará de alguns dados fora da banda e do modo de sinalização associado para indicar que o que você está enviando não são dados regulares, mas dados de erro. visto que o processo de origem já está em um estado imprevisível, isso pode ser pedir demais.
scytale
10
@JJC Para tergiversar com seu trocadilho, 3x é cerca de metade de uma ordem de magnitude, não uma terceira - sqrt (10) = ~ 3.
jab
0

Um recurso adicional Queue()disso é digno de nota é a rosca do alimentador. Esta seção observa "Quando um processo coloca um item pela primeira vez na fila, um encadeamento do alimentador é iniciado, que transfere objetos de um buffer para o tubo". Um número infinito de itens (ou tamanho máximo) pode ser inserido Queue()sem nenhuma chamada para queue.put()bloqueio. Isso permite que você armazene vários itens em um Queue(), até que seu programa esteja pronto para processá-los.

Pipe(), por outro lado, possui uma quantidade finita de armazenamento para itens que foram enviados para uma conexão, mas não foram recebidos da outra conexão. Depois que esse armazenamento é usado, as chamadas para connection.send()serão bloqueadas até que haja espaço para gravar o item inteiro. Isso interromperá a discussão fazendo a escrita até que outra discussão seja lida no tubo. ConnectionOs objetos fornecem acesso ao descritor de arquivo subjacente. Nos sistemas * nix, é possível impedir o connection.send()bloqueio de chamadas usando a os.set_blocking()função No entanto, isso causará problemas se você tentar enviar um único item que não se encaixa no arquivo do tubo. As versões recentes do Linux permitem aumentar o tamanho de um arquivo, mas o tamanho máximo permitido varia de acordo com as configurações do sistema. Portanto, você nunca deve confiar em Pipe()dados de buffer. Chamadas paraconnection.send poderia bloquear até que os dados fossem lidos do canal em algum outro lugar.

Em conclusão, a Fila é uma escolha melhor do que a tubulação quando você precisa armazenar dados em buffer. Mesmo quando você só precisa se comunicar entre dois pontos.

Roger Iyengar
fonte