Antrian. Antrian vs. collections.deque

181

Saya perlu antrian tempat beberapa utas dapat memasukkan berbagai hal, dan beberapa utas dapat dibaca.

Python memiliki setidaknya dua kelas antrian, Antrian. Antrian dan collections.deque, dengan yang pertama tampaknya menggunakan yang terakhir secara internal. Keduanya mengklaim aman dalam dokumentasi.

Namun, dokumen Antrian juga menyatakan:

collections.deque adalah implementasi alternatif dari antrian tidak terbatas dengan operasi append atom cepat () dan popleft () yang tidak memerlukan penguncian.

Yang saya kira saya tidak mengerti: Apakah ini berarti deque tidak sepenuhnya aman setelah semua?

Jika ya, saya mungkin tidak sepenuhnya memahami perbedaan antara kedua kelas. Saya dapat melihat bahwa Antrian menambahkan fungsi pemblokiran. Di sisi lain, ia kehilangan beberapa fitur deque seperti dukungan untuk operator.

Mengakses objek deque internal secara langsung, adalah

x dalam Antrian () .queque

aman?

Juga, mengapa Antrian menggunakan mutex untuk operasi itu ketika deque sudah thread-safe?

miracle2k
sumber
RuntimeError: deque mutated during iterationadalah apa yang bisa Anda dapatkan adalah menggunakan shared dequeantara beberapa utas dan tidak ada penguncian ...
toine
4
@ toine yang tidak ada hubungannya dengan utas sekalipun. Anda bisa mendapatkan kesalahan ini setiap kali Anda menambah / menghapus beberapa dequesaat iterasi bahkan di utas yang sama. Satu-satunya alasan Anda tidak bisa mendapatkan kesalahan ini Queueadalah karena Queuetidak mendukung iterasi.
maks

Jawaban:

281

Queue.Queuedan collections.dequemelayani berbagai tujuan. Queue.Queue dimaksudkan untuk memungkinkan utas berbeda berkomunikasi menggunakan pesan / data yang antri, sedangkan collections.dequehanya dimaksudkan sebagai struktur data. Itu sebabnya Queue.Queuememiliki metode seperti put_nowait(), get_nowait(), dan join(), sedangkan collections.dequetidak. Queue.Queuetidak dimaksudkan untuk digunakan sebagai koleksi, karena itu ia tidak memiliki suka inoperator.

Intinya begini: jika Anda memiliki banyak utas dan Anda ingin mereka dapat berkomunikasi tanpa perlu kunci, Anda sedang mencari Queue.Queue; jika Anda hanya ingin antrian atau antrian ganda sebagai datastructure, gunakan collections.deque.

Akhirnya, mengakses dan memanipulasi deque internal Queue.Queuebermain dengan api - Anda benar-benar tidak ingin melakukan itu.

Keith Gaughan
sumber
6
Tidak, itu sama sekali bukan ide yang bagus. Jika Anda melihat sumbernya Queue.Queue, ia menggunakan di dequebawah tenda. collections.dequeadalah koleksi, sedangkan Queue.Queuemekanisme komunikasi. Overhead Queue.Queueadalah untuk membuatnya threadsafe. Menggunakan dequeuntuk berkomunikasi di antara utas hanya akan menyebabkan balapan yang menyakitkan. Setiap kali dequeada threadsafe, itu adalah kecelakaan bahagia tentang bagaimana penerjemah diimplementasikan, dan bukan sesuatu yang bisa diandalkan. Itu sebabnya Queue.Queueada di tempat pertama.
Keith Gaughan
2
Hanya perlu diingat bahwa jika Anda berkomunikasi melintasi utas, Anda bermain api dengan menggunakan deque. deque adalah threadsafe secara tidak sengaja karena keberadaan GIL. Implementasi tanpa GIL akan memiliki karakteristik kinerja yang sama sekali berbeda, sehingga mengabaikan implementasi lain tidaklah bijaksana. Selain itu, sudahkah Anda menghitung waktu Antrian vs deque untuk digunakan di utas sebagai lawan tolok ukur naif penggunaannya dalam satu utas? Jika kode Anda adalah bahwa sensitif terhadap kecepatan Antrian vs deque, Python mungkin tidak menjadi bahasa yang Anda cari.
Keith Gaughan
3
@KeithGaughan deque is threadsafe by accident due to the existence of GIL; memang benar bahwa dequebergantung pada GIL untuk memastikan keamanan benang - tetapi tidak by accident. Dokumentasi python resmi dengan jelas menyatakan bahwa deque pop*/ append*metode aman-thread. Jadi setiap implementasi python yang valid harus memberikan jaminan yang sama (implementasi GIL-less harus mencari tahu bagaimana melakukannya tanpa GIL). Anda dapat dengan aman mengandalkan jaminan itu.
maks
2
@fantabolous komentar saya sebelumnya meskipun, saya tidak mengerti bagaimana Anda akan digunakan dequeuntuk komunikasi. Jika Anda membungkusnya popdengan try/except, Anda akan berakhir dengan loop sibuk memakan sejumlah besar CPU hanya menunggu data baru. Ini tampak seperti pendekatan yang sangat tidak efisien dibandingkan dengan panggilan pemblokiran yang ditawarkan oleh Queue, yang memastikan bahwa utas menunggu data akan tertidur dan tidak membuang waktu CPU.
maks
3
Anda mungkin ingin membaca kode sumber untuk Queue.Queuesaat itu, karena itu ditulis menggunakan collections.deque: hg.python.org/cpython/file/2.7/Lib/Queue.py - menggunakan variabel kondisi untuk secara efisien memungkinkan dequemembungkusnya diakses melewati batas utas dengan aman dan efisien. Penjelasan tentang bagaimana Anda akan menggunakan dequeuntuk komunikasi ada di sana di sumber.
Keith Gaughan
44

Jika semua yang Anda cari adalah cara aman untuk mentransfer objek di antara utas , maka keduanya akan berfungsi (baik untuk FIFO dan LIFO). Untuk FIFO:

catatan:

  • Operasi lain pada deque mungkin tidak aman, saya tidak yakin.
  • dequetidak memblokir pop()atau popleft()jadi Anda tidak dapat mendasarkan alur utas konsumen Anda pada pemblokiran sampai item baru tiba.

Namun, tampaknya deque memiliki keunggulan efisiensi yang signifikan . Berikut adalah beberapa hasil benchmark dalam hitungan detik menggunakan CPython 2.7.3 untuk menyisipkan dan menghapus item 100k

deque 0.0747888759791
Queue 1.60079066852

Inilah kode patokan:

import time
import Queue
import collections

q = collections.deque()
t0 = time.clock()
for i in xrange(100000):
    q.append(1)
for i in xrange(100000):
    q.popleft()
print 'deque', time.clock() - t0

q = Queue.Queue(200000)
t0 = time.clock()
for i in xrange(100000):
    q.put(1)
for i in xrange(100000):
    q.get()
print 'Queue', time.clock() - t0
Jonathan
sumber
1
Anda mengklaim bahwa "Operasi lain pada dequemungkin bukan thread yang aman". Dari mana Anda mendapatkannya?
Matt
@Matt - diucapkan ulang untuk menyampaikan makna saya dengan lebih baik
Jonathan
3
Ok terima kasih. Itu menghentikan saya dari menggunakan deque karena saya pikir Anda tahu sesuatu yang saya tidak tahu. Saya kira saya akan menganggap itu aman utas sampai saya menemukan sebaliknya.
Matt
@Matt "Operasi append (), appendleft (), pop (), popleft (), dan len (d) aman-threaded di CPython." sumber: bugs.python.org/issue15329
Filippo Vitale
7

Untuk informasi, ada tiket Python yang dirujuk untuk keamanan thread deque ( https://bugs.python.org/issue15329 ). Judul "memperjelas metode deque mana yang aman untuk thread"

Intinya di sini: https://bugs.python.org/issue15329#msg199368

Operasi append (), appendleft (), pop (), popleft (), dan len (d) aman di thready di CPython. Metode append memiliki DECREF pada akhirnya (untuk kasus di mana maxlen telah ditetapkan), tetapi ini terjadi setelah semua pembaruan struktur telah dibuat dan invarian telah dipulihkan, jadi tidak apa-apa untuk memperlakukan operasi ini sebagai atom.

Lagi pula, jika Anda tidak 100% yakin dan Anda lebih suka keandalan daripada kinerja, cukup cantumkan Kunci seperti;)

Serigala jahat
sumber
6

Semua metode elemen tunggal di dequeatomik dan aman. Semua metode lain juga aman untuk thread. Hal-hal seperti len(dq), dq[4]menghasilkan nilai yang benar sesaat. Tapi pikirkan misalnya tentang dq.extend(mylist): Anda tidak mendapatkan jaminan bahwa semua elemen masukmylist diajukan secara berurutan ketika utas lainnya juga menambahkan elemen di sisi yang sama - tetapi itu biasanya bukan persyaratan dalam komunikasi antar-utas dan untuk tugas yang dipertanyakan.

Jadi a deque~ 20x lebih cepat daripada Queue(yang menggunakan dequeunder the hood) dan kecuali Anda tidak memerlukan API sinkronisasi "nyaman" (pemblokiran / batas waktu), maxsizekepatuhan yang ketat atau "Abaikan metode ini (_put, _get, .. ) untuk mengimplementasikan organisasi lain " perilaku sub-klasifikasi antrian , atau ketika Anda mengurus hal-hal seperti itu sendiri, maka telanjangdeque adalah kesepakatan yang baik dan efisien untuk komunikasi antar-jalur berkecepatan tinggi.

Kenyataannya, penggunaan metode mutex ekstra dan metode ekstra ._get()dll. Yang banyak dilakukan Queue.pyadalah karena kendala kompatibilitas ke belakang, desain yang berlebihan di masa lalu, dan kurangnya perawatan untuk memberikan solusi yang efisien untuk masalah bottleneck kecepatan penting ini dalam komunikasi antar-benang. Daftar digunakan dalam versi Python yang lebih lama - tetapi bahkan list.append () /. Pop (0) adalah & adalah atomic dan threadsafe ...

kxr
sumber
3

Menambahkan notify_all()ke masing deque append- masing dan popleftmenghasilkan hasil yang jauh lebih buruk dequedaripada peningkatan 20x yang dicapai oleh dequeperilaku default :

deque + notify_all: 0.469802
Queue:              0.667279

@ Jonathan sedikit memodifikasi kodenya dan saya mendapatkan benchmark menggunakan cPython 3.6.2 dan menambahkan kondisi dalam deque loop untuk mensimulasikan perilaku yang dilakukan Queue.

import time
from queue import Queue
import threading
import collections

mutex = threading.Lock()
condition = threading.Condition(mutex)
q = collections.deque()
t0 = time.clock()
for i in range(100000):
    with condition:
        q.append(1)
        condition.notify_all()
for _ in range(100000):
    with condition:
        q.popleft()
        condition.notify_all()
print('deque', time.clock() - t0)

q = Queue(200000)
t0 = time.clock()
for _ in range(100000):
    q.put(1)
for _ in range(100000):
    q.get()
print('Queue', time.clock() - t0)

Dan sepertinya kinerja dibatasi oleh fungsi ini condition.notify_all()

collections.deque adalah implementasi alternatif dari antrian tidak terbatas dengan operasi append atom cepat () dan popleft () yang tidak memerlukan penguncian. dokumen Antrian

nikan1996
sumber
2

dequeaman dari benang. "operasi yang tidak memerlukan penguncian" berarti bahwa Anda tidak harus melakukan penguncian sendiri,deque mengurusnya.

Mengambil melihat pada Queuesumber, deque internal disebut self.queuedan menggunakan mutex untuk accesor dan mutasi, sehingga Queue().queueadalah tidak benang-aman untuk digunakan.

Jika Anda mencari operator "in", maka deque atau antrian mungkin bukan struktur data yang paling tepat untuk masalah Anda.

brian-brazil
sumber
1
Nah, yang ingin saya lakukan adalah memastikan bahwa tidak ada duplikat ditambahkan ke antrian. Apakah ini bukan sesuatu yang berpotensi didukung antrian?
miracle2k
1
Mungkin akan lebih baik untuk memiliki satu set terpisah, dan memperbaruinya ketika Anda menambahkan / menghapus sesuatu dari antrian. Itu akan menjadi O (log n) daripada O (n), tetapi Anda harus berhati-hati untuk menjaga set dan antrian tetap sinkron (yaitu mengunci).
brian-brazil
Set Python adalah tabel hash, jadi itu akan menjadi O (1). Tapi ya, Anda masih harus melakukan penguncian.
AFoglia
1

(sepertinya saya tidak memiliki reputasi untuk berkomentar ...) Anda harus berhati-hati metode deque mana yang Anda gunakan dari utas yang berbeda.

deque.get () tampaknya threadsafe, tetapi saya telah menemukan bahwa melakukan

for item in a_deque:
   process(item)

dapat gagal jika utas lainnya menambahkan item pada saat yang sama. Saya mendapat RuntimeException yang mengeluh "deque bermutasi selama iterasi".

Periksa collectionsmodule.c untuk melihat operasi mana yang dipengaruhi oleh ini

Eliot Blennerhassett
sumber
kesalahan semacam ini tidak khusus untuk utas dan utas-pengaman utama. Itu terjadi misalnya dengan hanya melakukan >>> di = {1:None} >>> for x in di: del di[x]
kxr
1
Pada dasarnya Anda tidak boleh mengulang-ulang sesuatu yang dapat dimodifikasi oleh utas lain (walaupun dalam beberapa kasus Anda bisa melakukannya dengan menambahkan perlindungan Anda sendiri). Seperti Antrian, Anda dimaksudkan untuk mengeluarkan / mengeluarkan item dari antrian sebelum memprosesnya, dan Anda biasanya melakukannya dengan whileloop.
fantabolous