Багатопроцесорна робота - Труба проти черги


151

Які принципові відмінності між чергами та трубами в багатопроцесорному пакеті Python ?

У яких сценаріях слід обирати один за іншим? Коли вигідно використовувати Pipe()? Коли вигідно використовувати Queue()?

Відповіді:


281
  • А Pipe()може мати лише дві кінцеві точки.

  • А Queue()може мати декількох виробників і споживачів.

Коли їх використовувати

Якщо для спілкування вам потрібно більше двох точок, використовуйте a Queue().

Якщо вам потрібні абсолютні показники роботи, Pipe()це набагато швидше, тому що Queue()будується на вершині Pipe().

Ефективність Бенчмаркінг

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

FYI, я кинув результати за 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()це приблизно втричі швидше, ніж a 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 () приблизно втричі швидше, ніж черга ()"
Джеймс Брейді

13
Відмінно! Гарна відповідь і приємно, що ви надали орієнтири! У мене є лише дві крихітні каламбури: (1) "на порядок швидше" - це трохи завищення. Різниця становить x3, що становить приблизно третину одного порядку величини. Просто кажу. ;-); та (2) більш справедливим порівнянням було б запущене N робітників, кожен з яких спілкувався з основною ниткою по трубі «точка-точка» порівняно з продуктивністю запущених N працівників, які все тягнуться з однієї черги на багатоточку.
JJC

3
До вашого "Бонусного матеріалу" ... Так. Якщо ви підкласифікуєте Процес, покладіть основну частину методу 'run' у спробу блоку. Це також корисний спосіб вести журнал виключень. Щоб повторити нормальний вихід виключення: sys.stderr.write (''. Join (traceback.format_exception (* (sys.exc_info ())))))
travc

2
@ alexpinho98 - але вам знадобляться деякі позадіапазонні дані та пов'язаний з ними режим сигналізації, щоб вказати, що те, що ви надсилаєте, - це не регулярні дані, а дані про помилки. бачачи, що процес початківців вже знаходиться у непередбачуваному стані, про це може бути занадто багато запитань.
scytale

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

1

Ще однією особливістю, Queue()яку варто відзначити, є подаюча нитка. У цьому розділі зазначається "Коли процес вперше ставить елемент у чергу, запускається нитка подачі, яка передає об'єкти з буфера в трубу". Нескінченна кількість (або максимізувати) елементів можна вставити в Queue()без будь-яких дзвінків на queue.put()блокування. Це дозволяє зберігати кілька елементів у програмі Queue(), поки ваша програма не буде готова обробити їх.

Pipe()з іншого боку, має обмежений обсяг пам’яті для елементів, надісланих до одного з'єднання, але не отриманих від іншого з'єднання. Після використання цього сховища дзвінки connection.send()блокуватимуться, поки не буде місця для запису всього елемента. Це буде затримувати нитку під час написання, поки якась інша нитка не прочитає з труби. ConnectionОб'єкти надають доступ до дескриптора базового файлу. У системах * nix ви можете запобігти connection.send()блокуванню дзвінків за допомогою os.set_blocking()функції. Однак це спричинить проблеми, якщо ви спробуєте надіслати один файл, який не вміщується у файл труби. Останні версії Linux дозволяють збільшити розмір файлу, але максимально дозволений розмір змінюється залежно від конфігурацій системи. Тому ніколи не слід покладатися на Pipe()буферні дані. Дзвінки доconnection.send може заблокувати, поки дані не будуть зчитуватися з труби щене.

На закінчення, черга є кращим вибором, ніж труба, коли вам потрібно буфер даних. Навіть коли вам потрібно спілкуватися лише між двома пунктами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.