Queue.Queue vs. collections.deque


181

Ho bisogno di una coda in cui più thread possono inserire roba e da cui possono leggere più thread.

Python ha almeno due classi di coda, Queue.Queue e collections.deque, con la prima che sembra usare la seconda internamente. Entrambi affermano di essere thread-safe nella documentazione.

Tuttavia, i documenti della coda indicano anche:

Collections.deque è un'implementazione alternativa di code illimitate con operazioni atom (append () e popleft () atomiche veloci che non richiedono il blocco.

Il che immagino di non capire affatto: questo significa che deque non è completamente sicuro per i thread dopo tutto?

In tal caso, potrei non comprendere appieno la differenza tra le due classi. Vedo che la coda aggiunge funzionalità di blocco. D'altra parte, perde alcune funzioni di deque come il supporto per l'operatore.

L'accesso diretto all'oggetto deque interno è

x in Queue (). deque

thread-safe?

Inoltre, perché Queue utilizza un mutex per le sue operazioni quando deque è già sicuro per i thread?


RuntimeError: deque mutated during iterationè quello che potresti ottenere è usare una condivisione dequetra più thread e nessun blocco ...
toine

4
@toine che non ha nulla a che fare con i thread. È possibile ottenere questo errore ogni volta che si aggiunge / elimina per un dequepo 'ripetendo lo stesso thread. L'unico motivo per cui non è possibile ottenere questo errore Queueè che Queuenon supporta l'iterazione.
massimo

Risposte:


281

Queue.Queuee collections.dequeservire a scopi diversi. Queue.Queue ha lo scopo di consentire a diversi thread di comunicare utilizzando messaggi / dati in coda, mentre collections.dequeè semplicemente inteso come una struttura dati. È per questo che Queue.Queueha metodi come put_nowait(), get_nowait()e join(), mentre collections.dequenon lo fa. Queue.Queuenon è inteso per essere usato come una raccolta, motivo per cui manca gente come l' inoperatore.

Si riduce a questo: se hai più thread e vuoi che siano in grado di comunicare senza la necessità di blocchi, stai cercando Queue.Queue; se si desidera solo una coda o una coda doppia come struttura dati, utilizzare collections.deque.

Infine, accedere e manipolare il deque interno di a Queue.Queuesta giocando con il fuoco - non vuoi davvero farlo.


6
No, non è affatto una buona idea. Se guardi la fonte di Queue.Queue, utilizza dequesotto il cofano. collections.dequeè una raccolta, mentre Queue.Queueè un meccanismo di comunicazione. Il sovraccarico Queue.Queueè quello di renderlo sicuro per i thread. L'uso dequeper comunicare tra i thread porterà solo a razze dolorose. Ogni volta che dequesembra essere sicuro del thread, questo è un felice incidente di come viene implementato l'interprete e non qualcosa su cui fare affidamento. Ecco perché Queue.Queueesiste in primo luogo.
Keith Gaughan,

2
Tieni presente che se stai comunicando attraverso i thread, stai giocando con il fuoco usando Deque. il deque è sicuro per errore a causa dell'esistenza del GIL. Un'implementazione senza GIL avrà caratteristiche prestazionali completamente diverse, quindi scontare altre implementazioni non è saggio. Inoltre, hai cronometrato Queue vs deque per l'uso tra thread anziché un ingenuo benchmark del suo utilizzo in un singolo thread? Se il tuo codice è così sensibile alla velocità di Queue vs deque, Python potrebbe non essere la lingua che stai cercando.
Keith Gaughan,

3
@KeithGaughan deque is threadsafe by accident due to the existence of GIL; è vero che dequesi affida a GIL per garantire la sicurezza dei thread, ma non lo è by accident. La documentazione ufficiale di Python afferma chiaramente che i metodi deque pop*/ append*sono thread-safe. Quindi qualsiasi implementazione valida di Python deve fornire la stessa garanzia (le implementazioni senza GIL dovranno capire come farlo senza GIL). Puoi contare su quelle garanzie in tutta sicurezza.
massimo

2
@fantabolous nonostante il mio commento precedente, non capisco bene come useresti dequeper la comunicazione. Se ti avvolgi popin a try/except, finirai con un loop occupato che consuma un'enorme quantità di CPU in attesa di nuovi dati. Questo sembra un approccio orribilmente inefficiente rispetto alle chiamate di blocco offerte da Queue, che assicurano che il thread in attesa di dati andrà in sospensione e non sprecherà il tempo della CPU.
massimo

3
Potresti voler leggere il codice sorgente per Queue.Queueallora, perché è scritto usando collections.deque: hg.python.org/cpython/file/2.7/Lib/Queue.py - utilizza variabili di condizione per consentire in modo efficiente l' dequeaccesso ai wrapper oltre i limiti del thread in modo sicuro ed efficiente. La spiegazione di come useresti a dequeper la comunicazione è proprio lì nella fonte.
Keith Gaughan,

44

Se tutto ciò che stai cercando è un modo thread-safe per trasferire oggetti tra thread , entrambi funzionerebbero (sia per FIFO che per LIFO). Per FIFO:

Nota:

  • Altre operazioni su dequepotrebbero non essere thread-safe, non ne sono sicuro.
  • dequenon si blocca pop()o popleft()quindi non è possibile basare il flusso del thread del consumatore sul blocco fino all'arrivo di un nuovo articolo.

Tuttavia, sembra che il deque abbia un notevole vantaggio in termini di efficienza . Ecco alcuni risultati di benchmark in pochi secondi utilizzando CPython 2.7.3 per l'inserimento e la rimozione di 100.000 elementi

deque 0.0747888759791
Queue 1.60079066852

Ecco il codice di riferimento:

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

1
Sostieni che "Altre operazioni su dequepotrebbero non essere thread-safe". Da dove lo prendi?
Matt,

@Matt - riformulato per trasmettere meglio il mio significato
Jonathan,

3
Ok grazie. Questo mi impediva di usare Deque perché pensavo che tu sapessi qualcosa che non sapevo. Suppongo che supporrò che sia sicuro per i thread fino a quando non scoprirò diversamente.
Matt

@Matt "Le operazioni append (), appendleft (), pop (), popleft () e len (d) di deque sono thread-safe in CPython." fonte: bugs.python.org/issue15329
Filippo Vitale,

7

Per informazioni esiste un ticket Python a cui si fa riferimento per la sicurezza thread deque ( https://bugs.python.org/issue15329 ). Titolo "chiarire quali metodi di deque sono thread-safe"

Bottom line qui: https://bugs.python.org/issue15329#msg199368

Le operazioni append (), appendleft (), pop (), popleft () e len (d) di deque sono thread-safe in CPython. I metodi append hanno un DECREF alla fine (per i casi in cui è stato impostato maxlen), ma ciò accade dopo che tutti gli aggiornamenti della struttura sono stati fatti e gli invarianti sono stati ripristinati, quindi va bene trattare queste operazioni come atomiche.

Ad ogni modo, se non sei sicuro al 100% e preferisci l'affidabilità rispetto alle prestazioni, inserisci un lucchetto simile;)


6

Tutti i metodi a elemento singolo dequesono atomici e thread-safe. Anche tutti gli altri metodi sono thread-safe. Cose come len(dq), dq[4]producono valori corretti momentanei. Ma pensate ad esempio a dq.extend(mylist): non avrete la garanzia che tutti gli elementi in mylistsiano archiviati in una riga quando anche altri thread aggiungono elementi dallo stesso lato, ma di solito non è un requisito nella comunicazione tra thread e per l'attività in questione.

Quindi a dequeè ~ 20 volte più veloce di Queue(che usa un dequeunder the hood) e, a meno che non sia necessaria l'API di sincronizzazione "comoda" (blocco / timeout), il rigoroso maxsizerispetto o il "Sovrascrivi questi metodi (_put, _get, .. ) per implementare il comportamento di sottoclasse di altre organizzazioni di code o quando ci si prende cura di tali cose da soli, allora uno scoperto dequeè un buon affare ed efficiente per la comunicazione inter-thread ad alta velocità.

In effetti, il pesante uso di un extra mutex e di un metodo aggiuntivo, ._get()ecc., Richiede chiamate di metodo a Queue.pycausa di vincoli di compatibilità all'indietro, sovrastima progettazione e mancanza di cura nel fornire una soluzione efficiente per questo importante problema di colli di bottiglia della velocità nella comunicazione tra thread. Un elenco era usato nelle versioni precedenti di Python, ma anche list.append () /. Pop (0) era & è atomico e thread sicuro ...


3

Aggiunta notify_all()di ciascuna deque appende popleftdetermina risultati molto peggiori per dequequanto il miglioramento conseguito 20x di default dequecomportamento :

deque + notify_all: 0.469802
Queue:              0.667279

@Jonathan modifica un po 'il suo codice e ottengo il benchmark usando cPython 3.6.2 e aggiungo la condizione in deque loop per simulare il comportamento della coda.

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)

E sembra che le prestazioni siano limitate da questa funzione condition.notify_all()

collections.deque è un'implementazione alternativa di code illimitate con operazioni atom (append () e popleft () atomiche veloci che non richiedono il blocco. coda documenti


2

dequeè thread-safe. "operazioni che non richiedono il blocco" significa che non è necessario eseguire il blocco da soli, se dequene occupa.

Dando un'occhiata alla Queuefonte, viene chiamato il deque interno self.queuee utilizza un mutex per gli accessori e le mutazioni, quindi nonQueue().queue è sicuro da usare.

Se stai cercando un operatore "in", una deque o una coda non è probabilmente la struttura di dati più appropriata per il tuo problema.


1
Bene, quello che voglio fare è assicurarmi che non vengano aggiunti duplicati alla coda. Non è qualcosa che una coda potrebbe potenzialmente supportare?
miracle2k,

1
Probabilmente sarebbe meglio avere un set separato e aggiornarlo quando aggiungi / rimuovi qualcosa dalla coda. Sarà O (log n) anziché O (n), ma dovrai fare attenzione a mantenere sincronizzati set e coda (cioè blocco).
Brasile-Brasile

Un set Python è una tabella hash, quindi sarebbe O (1). Ma sì, dovresti comunque fare il blocco.
AFoglia,

1

(sembra che non abbia la reputazione di commentare ...) Devi stare attento a quali metodi del deque usi da diversi thread.

deque.get () sembra essere thread-safe, ma l'ho scoperto

for item in a_deque:
   process(item)

può fallire se un altro thread aggiunge elementi contemporaneamente. Ho ricevuto una RuntimeException che si lamentava "deque mutated durante iteration".

Dai un'occhiata collectionsmodule.c per vedere quali operazioni sono interessate da questo


questo tipo di errore non è speciale per i thread e la sicurezza dei thread principali. Succede ad esempio semplicemente facendo >>> di = {1:None} >>> for x in di: del di[x]
kxr

1
Fondamentalmente non dovresti mai fare il loop su qualcosa che potrebbe essere modificato da un altro thread (anche se in alcuni casi potresti farlo aggiungendo la tua protezione). Come una coda, hai intenzione di estrarre / estrarre un elemento dalla coda prima di elaborarlo e normalmente lo faresti con un whileciclo.
fantabolous
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.