Multiprocessing - Pipa vs Antrian

Jawaban:

281
  • A Pipe()hanya dapat memiliki dua titik akhir.

  • A Queue()dapat memiliki banyak produsen dan konsumen.

Kapan menggunakannya

Jika Anda membutuhkan lebih dari dua poin untuk berkomunikasi, gunakan a Queue().

Jika Anda membutuhkan kinerja absolut, a Pipe()jauh lebih cepat karena Queue()dibangun di atas Pipe().

Benchmarking Kinerja

Mari kita asumsikan Anda ingin menelurkan dua proses dan mengirim pesan di antara mereka secepat mungkin. Ini adalah hasil pengaturan waktu dari drag race antara pengujian yang sama menggunakan Pipe()dan Queue()... Ini ada pada ThinkpadT61 yang menjalankan Ubuntu 11.10, dan Python 2.7.2.

FYI, saya memberikan hasil JoinableQueue()sebagai bonus; JoinableQueue()menyumbang tugas ketika queue.task_done()dipanggil (bahkan tidak tahu tentang tugas tertentu, itu hanya menghitung tugas yang belum selesai dalam antrian), sehingga yang queue.join()tahu pekerjaan selesai.

Kode untuk masing-masing di bagian bawah jawaban ini ...

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

Singkatnya Pipe() sekitar tiga kali lebih cepat dari a Queue(). Jangan pernah berpikir tentang JoinableQueue()kecuali Anda benar-benar harus memiliki manfaat.

MATERI BONUS 2

Multiprocessing memperkenalkan perubahan halus dalam aliran informasi yang menyulitkan proses debug kecuali Anda tahu beberapa pintasan. Misalnya, Anda mungkin memiliki skrip yang berfungsi dengan baik saat mengindeks melalui kamus dalam banyak kondisi, tetapi jarang gagal dengan input tertentu.

Biasanya kita mendapatkan petunjuk tentang kegagalan ketika seluruh proses python crash; namun, Anda tidak mendapatkan traceback macet yang tidak diinginkan dicetak ke konsol jika fungsi multiprosesing macet. Melacak crash multiprocessing yang tidak diketahui tidak mudah tanpa petunjuk apa yang menyebabkan crash.

Cara paling sederhana yang saya temukan untuk melacak informasi macet multiprosesor adalah dengan membungkus seluruh fungsi multiprosesing dalam try/ exceptdan menggunakan 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()

Sekarang, ketika Anda menemukan kerusakan Anda melihat sesuatu seperti:

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'

Kode sumber:


"""
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
sumber
2
@Jonathan "Singkatnya Pipe () adalah sekitar tiga kali lebih cepat dari Antrian ()"
James Brady
13
Luar biasa! Jawaban bagus dan bagus yang Anda berikan tolok ukur! Saya hanya punya dua quibbles kecil: (1) "order magnitude lebih cepat" agak berlebihan. Perbedaannya adalah x3, yaitu sekitar sepertiga dari satu urutan besarnya. Hanya mengatakan. ;-); dan (2) perbandingan yang lebih adil adalah menjalankan pekerja N, masing-masing berkomunikasi dengan utas utama melalui pipa point-to-point dibandingkan dengan kinerja menjalankan pekerja N semua menarik dari antrian point-to-multipoint tunggal.
JJC
3
Untuk "Materi Bonus" Anda ... Ya. Jika Anda subkelas Proses, masukkan sebagian besar metode 'jalankan' di blok coba. Itu juga merupakan cara yang berguna untuk melakukan pencatatan pengecualian. Untuk mereplikasi output pengecualian normal: sys.stderr.write (''. Join (traceback.format_exception (* (sys.exc_info ())))))
travc
2
@ alexpinho98 - tetapi Anda akan memerlukan beberapa data out-of-band, dan mode pensinyalan terkait, untuk menunjukkan bahwa apa yang Anda kirim bukan data biasa tetapi data kesalahan. mengingat proses awalnya sudah dalam keadaan yang tidak dapat diprediksi, ini mungkin terlalu banyak untuk ditanyakan.
scytale
10
@JJC Untuk berdalih dengan berdalih Anda, 3x adalah sekitar setengah urutan besarnya, bukan sepertiga - sqrt (10) = ~ 3.
jab
1

Satu fitur tambahan Queue()yang perlu diperhatikan adalah utas pengumpan. Bagian ini mencatat "Ketika suatu proses pertama kali menempatkan item pada antrian, sebuah thread feeder dimulai yang mentransfer objek dari buffer ke dalam pipa." Jumlah item (atau maksimum) yang tak terbatas dapat dimasukkan Queue()tanpa harus queue.put()diblokir. Ini memungkinkan Anda untuk menyimpan banyak item dalam Queue(), hingga program Anda siap memprosesnya.

Pipe(), di sisi lain, memiliki jumlah penyimpanan terbatas untuk item yang telah dikirim ke satu koneksi, tetapi belum diterima dari koneksi lainnya. Setelah penyimpanan ini habis, panggilan ke connection.send()akan diblokir sampai ada ruang untuk menulis seluruh item. Ini akan menghentikan utas melakukan penulisan sampai beberapa utas lainnya membaca dari pipa. Connectionobjek memberi Anda akses ke deskriptor file yang mendasarinya. Pada sistem * nix, Anda dapat mencegah connection.send()pemblokiran panggilan menggunakan os.set_blocking()fungsi ini. Namun, ini akan menimbulkan masalah jika Anda mencoba mengirim satu item yang tidak sesuai dengan file pipa. Versi Linux terbaru memungkinkan Anda meningkatkan ukuran file, tetapi ukuran maksimum yang dibolehkan bervariasi berdasarkan konfigurasi sistem. Karena itu Anda tidak boleh mengandalkanPipe() buffer data. Panggilan untukconnection.send dapat memblokir sampai data dibaca dari pipa di tempat lain.

Kesimpulannya, Antrian adalah pilihan yang lebih baik daripada pipa ketika Anda perlu melakukan buffer data. Bahkan ketika Anda hanya perlu berkomunikasi antara dua poin.

Roger Iyengar
sumber