Многопроцессорная обработка - труба против очереди


151

Каковы принципиальные различия между очередями и каналами в многопроцессорном пакете Python ?

В каких сценариях следует выбирать один над другим? Когда выгодно использовать Pipe()? Когда выгодно использовать Queue()?

Ответы:


281
  • А Pipe()может иметь только две конечные точки.

  • А Queue()может иметь несколько производителей и потребителей.

Когда их использовать

Если вам нужно больше двух точек для общения, используйте Queue().

Если вам нужна абсолютная производительность, Pipe()намного быстрее, потому что Queue()построен на вершине Pipe().

Сравнительный анализ производительности

Предположим, вы хотите порождать два процесса и посылать сообщения между ними как можно быстрее. Это временные результаты гонок между аналогичными тестами Pipe()и Queue()... Это на ThinkpadT61 под управлением Ubuntu 11.10 и Python 2.7.2.

К вашему сведению, я добавил результаты JoinableQueue()в качестве бонуса; JoinableQueue()учитывает задачи при queue.task_done()вызове (он даже не знает о конкретной задаче, он просто считает незавершенные задачи в очереди), поэтому он queue.join()знает, что работа завершена.

Код для каждого в нижней части этого ответа ...

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

В итоге Pipe()примерно в три раза быстрее, чем Queue(). Даже не думайте о том, что JoinableQueue()вы действительно должны иметь преимущества.

БОНУС МАТЕРИАЛ 2

Многопроцессорная обработка вносит незначительные изменения в поток информации, что затрудняет отладку, если вы не знаете каких-либо ярлыков. Например, у вас может быть сценарий, который отлично работает при индексации через словарь при многих условиях, но редко дает сбой при определенных входных данных.

Обычно мы получаем подсказки об ошибке, когда происходит сбой всего процесса Python; однако, вы не получите незапрошенные трассировочные сообщения о сбоях, напечатанные на консоли, если происходит сбой многопроцессорной функции. Отслеживать неизвестные многопроцессорные сбои сложно, даже не зная, что привело к сбою процесса.

Самый простой способ, который я нашел, чтобы отследить информацию о сбое многопроцессорной обработки, - это обернуть всю многопроцессорную функцию в try/ exceptи использовать 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()

Теперь, когда вы видите сбой, вы видите что-то вроде:

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'

Исходный код:


"""
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 «В итоге Pipe () примерно в три раза быстрее, чем Queue ()»
Джеймс Брэди,

13
Превосходно! Хороший ответ и приятно, что вы предоставили ориентиры! У меня только две крошечные придирки: (1) «на порядок быстрее» - это немного преувеличение. Разница составляет х3, что составляет около трети от одного порядка. Просто говорю. ;-); и (2) более справедливым сравнением будет выполнение N рабочих, каждый из которых взаимодействует с основным потоком через канал «точка-точка», по сравнению с производительностью выполнения N рабочих, которые извлекают все данные из одной очереди в несколько точек.
JJC

3
На ваш "Бонусный материал" ... Да. Если вы создаете подклассы Process, поместите большую часть метода run в блок try. Это также полезный способ ведения журнала исключений. Для репликации нормального вывода исключений: sys.stderr.write (''. Join (traceback.format_exception (* (sys.exc_info ()))))
travc

2
@ alexpinho98 - но вам потребуются некоторые внеполосные данные и связанный с ними режим сигнализации, чтобы указать, что вы отправляете не обычные данные, а данные об ошибках. видя, что исходный процесс уже находится в непредсказуемом состоянии, это может быть слишком много, чтобы просить.
Сцена

10
@JJC придираться с вашим каламбур, 3x около половины порядка, а не третий - SQRT (10) = ~ 3.
прививка

1

Queue()Стоит отметить еще одну особенность - поток фидера. В этом разделе отмечается: «Когда процесс впервые помещает элемент в очередь, запускается поток фидера, который передает объекты из буфера в канал». В бесконечное количество (или maxsize) элементов можно вставлять Queue()без каких-либо призывов к queue.put()блокировке. Это позволяет хранить несколько элементов в Queue()папке до тех пор, пока ваша программа не будет готова их обработать.

Pipe()с другой стороны, имеет ограниченный объем памяти для элементов, которые были отправлены на одно соединение, но не были получены от другого соединения. После того, как это хранилище израсходовано, вызовы connection.send()блокируются, пока не останется места для записи всего элемента. Это остановит поток, делающий запись, пока какой-то другой поток не прочитает из канала. Connectionобъекты дают вам доступ к базовому файловому дескриптору. В системах * nix вы можете предотвратить connection.send()блокировку вызовов с помощью этой os.set_blocking()функции. Однако это вызовет проблемы, если вы попытаетесь отправить один элемент, который не помещается в файл канала. Последние версии Linux позволяют увеличивать размер файла, но максимально допустимый размер зависит от конфигурации системы. Поэтому вы никогда не должны полагаться на Pipe()буферизацию данных. Звонки вconnection.send может блокироваться до тех пор, пока данные не будут прочитаны из канала.

В заключение, Queue - лучший выбор, чем pipe, когда вам нужно буферизовать данные. Даже когда вам нужно общаться только между двумя точками.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.