Quais são as diferenças fundamentais entre filas e pipes no pacote de multiprocessamento do Python ?
Em quais cenários um deve escolher um sobre o outro? Quando é vantajoso usar Pipe()
? Quando é vantajoso usar Queue()
?
Quais são as diferenças fundamentais entre filas e pipes no pacote de multiprocessamento do Python ?
Em quais cenários um deve escolher um sobre o outro? Quando é vantajoso usar Pipe()
? Quando é vantajoso usar Queue()
?
Respostas:
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
/ except
e 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)))
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. Connection
Os 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.