Queue.Queue vs. collection.deque

179

Ich brauche eine Warteschlange, in die mehrere Threads Inhalte einfügen können und aus der mehrere Threads lesen können.

Python hat mindestens zwei Warteschlangenklassen, Queue.Queue und collection.deque, wobei die erstere anscheinend die letztere intern verwendet. Beide behaupten, in der Dokumentation threadsicher zu sein.

In den Warteschlangendokumenten heißt es jedoch auch:

collection.deque ist eine alternative Implementierung von unbegrenzten Warteschlangen mit schnellen atomaren append () - und popleft () -Operationen, für die kein Sperren erforderlich ist.

Was ich wohl nicht ganz verstehe: Bedeutet das, dass Deque doch nicht vollständig threadsicher ist?

Wenn ja, kann ich den Unterschied zwischen den beiden Klassen nicht vollständig verstehen. Ich kann sehen, dass die Warteschlange Blockierungsfunktionen hinzufügt. Auf der anderen Seite verliert es einige Deque-Funktionen wie die Unterstützung des In-Operators.

Der direkte Zugriff auf das interne Deque-Objekt ist

x in Queue (). deque

Thread-sicher?

Warum verwendet Queue einen Mutex für seine Operationen, wenn deque bereits threadsicher ist?

miracle2k
quelle
RuntimeError: deque mutated during iterationist, was Sie bekommen könnten, ist die Verwendung eines dequevon mehreren Threads gemeinsam genutzten und ohne Sperren ...
toine
4
@toine, das hat aber nichts mit Threads zu tun. Sie können diesen Fehler immer dann erhalten, wenn Sie eine hinzufügen oder löschen, dequewährend Sie selbst im selben Thread iterieren. Der einzige Grund, warum Sie diesen Fehler nicht erhalten können, Queueist, dass Queuedie Iteration nicht unterstützt wird.
Max

Antworten:

278

Queue.Queueund collections.dequedienen verschiedenen Zwecken. Queue.Queue ist dafür vorgesehen, dass verschiedene Threads mithilfe von Nachrichten / Daten in der Warteschlange kommunizieren können, während sie collections.dequelediglich als Datenstruktur gedacht sind. Warum das Queue.Queuehat Methoden wie put_nowait(), get_nowait()und join(), während collections.dequedies nicht tut. Queue.Queueist nicht dazu gedacht, als Sammlung verwendet zu werden, weshalb es an den inBetreibern mangelt .

Es läuft darauf hinaus: Wenn Sie mehrere Threads haben und möchten, dass diese ohne Sperren kommunizieren können, suchen Sie nach Queue.Queue; Wenn Sie nur eine Warteschlange oder eine Warteschlange mit zwei Enden als Datenstruktur möchten, verwenden Sie collections.deque.

Schließlich spielt der Zugriff auf und die Manipulation der internen Deque von a Queue.Queuemit dem Feuer - das wollen Sie wirklich nicht.

Keith Gaughan
quelle
6
Nein, das ist überhaupt keine gute Idee. Wenn Sie sich die Quelle von ansehen Queue.Queue, wird sie dequeunter der Haube verwendet. collections.dequeist eine Sammlung, während Queue.Queuees sich um einen Kommunikationsmechanismus handelt. Der Aufwand Queue.Queuebesteht darin, es threadsicher zu machen. Die Verwendung dequefür die Kommunikation zwischen Threads führt nur zu schmerzhaften Rennen. Wann dequeimmer es threadsicher ist, ist dies ein glücklicher Zufall, wie der Interpreter implementiert wird, und nicht etwas, auf das man sich verlassen kann. Deshalb Queue.Queuegibt es überhaupt.
Keith Gaughan
2
Denken Sie daran, dass Sie bei der Kommunikation über mehrere Threads hinweg mit Feuer spielen, indem Sie Deque verwenden. deque ist aufgrund der Existenz der GIL versehentlich threadsicher . Eine Implementierung ohne GIL weist völlig andere Leistungsmerkmale auf, daher ist es nicht ratsam, andere Implementierungen zu diskontieren. Haben Sie außerdem Queue vs Deque für die Verwendung über Threads hinweg zeitlich festgelegt, im Gegensatz zu einem naiven Benchmark für die Verwendung in einem einzelnen Thread? Wenn Ihr Code ist , dass empfindlich auf die Geschwindigkeit der Queue vs deque, könnte Python nicht die Sprache , die Sie suchen.
Keith Gaughan
3
@KeithGaughan deque is threadsafe by accident due to the existence of GIL; Es ist wahr, dass man dequesich auf GIL verlässt, um die Thread-Sicherheit zu gewährleisten - aber das ist es nicht by accident. In der offiziellen Python-Dokumentation heißt es eindeutig, dass deque pop*/ append*Methoden threadsicher sind. Daher muss jede gültige Python-Implementierung dieselbe Garantie bieten (GIL-freie Implementierungen müssen herausfinden, wie dies ohne GIL möglich ist). Auf diese Garantien können Sie sich sicher verlassen.
Max
2
@fantabolous Trotz meines vorherigen Kommentars verstehe ich nicht ganz, wie Sie dequefür die Kommunikation verwenden würden. Wenn Sie sich popin a einwickeln try/except, wird eine Besetztschleife enden, die eine enorme Menge an CPU verbraucht und nur auf neue Daten wartet. Dies scheint ein schrecklich ineffizienter Ansatz im Vergleich zu den von angebotenen Blockierungsaufrufen zu sein Queue, die sicherstellen, dass der auf Daten wartende Thread in den Ruhezustand wechselt und keine CPU-Zeit verschwendet.
Max
3
Vielleicht möchten Sie den Quellcode für diesen Queue.QueueZeitpunkt lesen , da er wie folgt geschrieben wurde collections.deque: hg.python.org/cpython/file/2.7/Lib/Queue.py - Er verwendet Bedingungsvariablen, um den effizienten dequeZugriff auf die it-Wraps zu ermöglichen sicher und effizient über Fadengrenzen hinweg. Die Erklärung, wie Sie a dequefür die Kommunikation verwenden würden, finden Sie direkt in der Quelle.
Keith Gaughan
43

Wenn Sie nur nach einer thread-sicheren Methode suchen , um Objekte zwischen Threads zu übertragen , funktionieren beide (sowohl für FIFO als auch für LIFO). Für FIFO:

Hinweis:

  • Andere Operationen sind dequemöglicherweise nicht threadsicher, ich bin mir nicht sicher.
  • dequeblockiert nicht pop()oder popleft()so können Sie Ihren Consumer-Thread-Fluss nicht auf das Blockieren stützen, bis ein neuer Artikel eintrifft.

Es scheint jedoch, dass Deque einen signifikanten Effizienzvorteil hat . Hier sind einige Benchmark-Ergebnisse in Sekunden mit CPython 2.7.3 zum Einfügen und Entfernen von 100.000 Elementen

deque 0.0747888759791
Queue 1.60079066852

Hier ist der Benchmark-Code:

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
quelle
1
Sie behaupten, dass "Andere Vorgänge für dequemöglicherweise nicht threadsicher sind". Woher bekommst du das?
Matt
@ Matt - umformuliert, um meine Bedeutung besser zu vermitteln
Jonathan
3
OK danke. Das hinderte mich daran, Deque zu verwenden, weil ich dachte, Sie wüssten etwas, was ich nicht wusste. Ich gehe einfach davon aus, dass es threadsicher ist, bis ich etwas anderes entdecke.
Matt
@Matt "Die Operationen append (), appendleft (), pop (), popleft () und len (d) der Deque sind in CPython threadsicher." Quelle: bugs.python.org/issue15329
Filippo Vitale
7

Zur Information gibt es ein Python-Ticket, auf das für die Sicherheit von Deque-Threads verwiesen wird ( https://bugs.python.org/issue15329 ). Titel "Klarstellen, welche Deque-Methoden threadsicher sind"

Fazit hier: https://bugs.python.org/issue15329#msg199368

Die Operationen append (), appendleft (), pop (), popleft () und len (d) der Deque sind in CPython threadsicher. Die Append-Methoden haben am Ende ein DECREF (für Fälle, in denen Maxlen festgelegt wurde). Dies geschieht jedoch, nachdem alle Strukturaktualisierungen vorgenommen und die Invarianten wiederhergestellt wurden. Daher ist es in Ordnung, diese Operationen als atomar zu behandeln.

Wie auch immer, wenn Sie nicht 100% sicher sind und Zuverlässigkeit der Leistung vorziehen, setzen Sie einfach ein ähnliches Schloss;)

BadWolf
quelle
5

Alle Einzelelementmethoden dequesind atomar und threadsicher. Alle anderen Methoden sind ebenfalls threadsicher. Solche Dinge len(dq), dq[4]ergeben momentane richtige Werte. Aber denken Sie zB an dq.extend(mylist): Sie erhalten keine Garantie dafür, dass alle Elemente in mylisteiner Reihe abgelegt werden, wenn andere Threads auch Elemente auf derselben Seite anhängen - dies ist jedoch normalerweise keine Voraussetzung für die Kommunikation zwischen Threads und für die fragliche Aufgabe.

A dequeist also ~ 20x schneller als Queue(was a dequeunter der Haube verwendet) und es sei denn, Sie benötigen nicht die "komfortable" Synchronisations-API (Blockieren / Timeout), die strikte maxsizeEinhaltung oder die "Diese Methoden überschreiben" (_put, _get, .. ) um das Unterklassifizierungsverhalten anderer Warteschlangenorganisationen zu implementieren , oder wenn Sie sich selbst um solche Dinge kümmern, ist ein Bare dequeein gutes und effizientes Geschäft für die Hochgeschwindigkeitskommunikation zwischen Threads.

Tatsächlich ist die häufige Verwendung eines zusätzlichen Mutex und einer zusätzlichen Methode ._get()usw. Queue.pyauf Abwärtskompatibilitätsbeschränkungen, frühere Überkonstruktionen und mangelnde Sorgfalt bei der Bereitstellung einer effizienten Lösung für dieses wichtige Problem mit Geschwindigkeitsengpässen bei der Kommunikation zwischen Threads zurückzuführen. In älteren Python-Versionen wurde eine Liste verwendet - aber auch list.append () /. Pop (0) war & ist atomar und threadsicher ...

kxr
quelle
3

Hinzufügen notify_all()zu jedem deque appendund popleftführt zu weit schlechteren Ergebnissen für dequeals die 20x Verbesserung durch Standard erreicht dequeVerhalten :

deque + notify_all: 0.469802
Queue:              0.667279

@ Jonathan ändert seinen Code ein wenig und ich erhalte den Benchmark mit cPython 3.6.2 und füge eine Bedingung in der Deque-Schleife hinzu, um das Verhalten der Warteschlange zu simulieren.

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)

Und es scheint, dass die Leistung durch diese Funktion begrenzt ist condition.notify_all()

collection.deque ist eine alternative Implementierung unbegrenzter Warteschlangen mit schnellen atomaren append () - und popleft () -Operationen, für die kein Sperren erforderlich ist. docs Queue

nikan1996
quelle
2

dequeist threadsicher. "Vorgänge, für die kein Sperren erforderlich ist" bedeutet, dass Sie das Sperren nicht selbst durchführen müssen, sondern sich darum dequekümmern.

Wirft man einen Blick auf die QueueQuelle, wird der interne deque genannt self.queueund verwendet ein Mutex für Zugriffs- und Mutationen, so Queue().queueist nicht zu verwenden Thread-sicher.

Wenn Sie nach einem "In" -Operator suchen, ist eine Deque oder Warteschlange möglicherweise nicht die am besten geeignete Datenstruktur für Ihr Problem.

Brian-Brasilien
quelle
1
Nun, ich möchte sicherstellen, dass der Warteschlange keine Duplikate hinzugefügt werden. Ist dies nicht etwas, das eine Warteschlange möglicherweise unterstützen könnte?
miracle2k
1
Es ist wahrscheinlich am besten, einen separaten Satz zu haben und diesen zu aktualisieren, wenn Sie etwas zur Warteschlange hinzufügen oder daraus entfernen. Das ist O (log n) und nicht O (n), aber Sie müssen vorsichtig sein, um das Set und die Warteschlange synchron zu halten (dh zu sperren).
Brian-Brasilien
Ein Python-Set ist eine Hash-Tabelle, also wäre es O (1). Aber ja, Sie müssten immer noch sperren.
AFoglia
1

(Anscheinend habe ich keinen Ruf zu kommentieren ...) Sie müssen vorsichtig sein, welche Methoden der Deque Sie aus verschiedenen Threads verwenden.

deque.get () scheint threadsicher zu sein, aber ich habe festgestellt, dass dies der Fall ist

for item in a_deque:
   process(item)

kann fehlschlagen, wenn ein anderer Thread gleichzeitig Elemente hinzufügt. Ich habe eine RuntimeException erhalten, die sich über "während der Iteration mutierte Deque" beschwert hat.

Überprüfen Sie collectionsmodule.c, um festzustellen, welche Vorgänge davon betroffen sind

Eliot Blennerhassett
quelle
Diese Art von Fehler ist nicht speziell für Gewinde und die Hauptgewindesicherheit. Es passiert zB einfach durch >>> di = {1:None} >>> for x in di: del di[x]
kxr
1
Grundsätzlich sollten Sie niemals eine Schleife über etwas durchlaufen, das möglicherweise von einem anderen Thread geändert wurde (obwohl Sie dies in einigen Fällen tun können, indem Sie Ihren eigenen Schutz hinzufügen). Wie bei einer Warteschlange sollen Sie ein Element vor der Verarbeitung aus der Warteschlange entfernen, und dies würden Sie normalerweise mit einer whileSchleife tun .
fantastisch