J'ai besoin d'une file d'attente dans laquelle plusieurs threads peuvent mettre des éléments et dans laquelle plusieurs threads peuvent lire.
Python a au moins deux classes de file d'attente, Queue.Queue et collections.deque, la première utilisant apparemment la dernière en interne. Les deux prétendent être thread-safe dans la documentation.
Cependant, la documentation de la file d'attente indique également:
collections.deque est une implémentation alternative des files d'attente non limitées avec des opérations atomiques rapides append () et popleft () qui ne nécessitent pas de verrouillage.
Ce qui, je suppose, ne comprend pas tout à fait: cela signifie-t-il que deque n'est pas totalement thread-safe après tout?
Si c'est le cas, je ne comprendrai peut-être pas bien la différence entre les deux classes. Je peux voir que Queue ajoute une fonctionnalité de blocage. D'un autre côté, il perd certaines caractéristiques telles que le support de l'opérateur.
Accéder directement à l’objet deque interne, c’est
x en file d'attente (). deque
fil-safe?
Aussi, pourquoi Queue utilise-t-il un mutex pour ses opérations alors que deque est déjà thread-safe?
Queue.Queue
Et collections.deque
Ont des objectifs différents. Queue.Queue est conçu pour permettre à différents threads de communiquer à l'aide de messages/données en file d'attente, alors que collections.deque
Est simplement conçu comme une structure de données. C'est pourquoi Queue.Queue
A des méthodes comme put_nowait()
, get_nowait()
et join()
, alors que collections.deque
N'en a pas. Queue.Queue
N'est pas destiné à être utilisé en tant que collection. C'est pourquoi il lui manque les caractéristiques de l'opérateur in
.
Cela revient à ceci: si vous avez plusieurs threads et que vous voulez qu'ils puissent communiquer sans avoir besoin de verrous, vous cherchez Queue.Queue
; si vous voulez juste une file d'attente ou une file d'attente à deux extrémités comme structure de données, utilisez collections.deque
.
Enfin, accéder et manipuler le deque interne d'un Queue.Queue
, C'est jouer avec le feu - vous ne voulez vraiment pas le faire.
Si tout ce que vous cherchez est un moyen sûr de threads pour transférer des objets entre des threads , les deux fonctionneraient (les deux pour FIFO et LIFO) .Pour FIFO:
Remarque:
deque
pourraient ne pas être thread-safe, je ne suis pas sûr.deque
ne bloque pas sur pop()
ou popleft()
, vous ne pouvez donc pas baser votre flux de thread consommateur sur le blocage jusqu'à l'arrivée d'un nouvel élément.Cependant, il semble que deque présente un avantage d'efficacité important . Voici quelques résultats de référence en quelques secondes en utilisant CPython 2.7.3 pour insérer et supprimer des éléments de 100 000 éléments.
deque 0.0747888759791
Queue 1.60079066852
Voici le code de référence:
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
Pour information, il existe un Python référencé pour deque thread-safety ( https://bugs.python.org/issue15329 ). Le titre "précise quelles méthodes deque sont thread-safe "
Ligne de fond ici: https://bugs.python.org/issue15329#msg199368
Les opérations append (), appendleft (), pop (), popleft () et len (d) de deque sont thread-safe dans CPython. Les méthodes append ont un DECREF à la fin (pour les cas où maxlen a été défini), mais cela se produit une fois que toutes les mises à jour de la structure ont été effectuées et que les invariants ont été restaurés. Vous pouvez donc traiter ces opérations comme des opérations atomiques.
Quoi qu'il en soit, si vous n'êtes pas sûr à 100% et que vous préférez la fiabilité à la performance, il vous suffit de mettre un verrou similaire à;
Toutes les méthodes à élément unique sur deque
sont atomiques et thread-safe. Toutes les autres méthodes sont également thread-safe. Des choses comme len(dq)
, dq[4]
Donnent des valeurs correctes momentanées. Mais pensez par exemple à propos de dq.extend(mylist)
: vous n’obtenez pas la garantie que tous les éléments de mylist
sont classés dans une rangée lorsque d’autres threads ajoutent également des éléments du même côté - mais c’est généralement pas obligatoire -read communication et pour la tâche en question.
Donc, un deque
est environ 20 fois plus rapide que Queue
(qui utilise un deque
sous le capot) et à moins que vous n'ayez pas besoin de l'API de synchronisation "confortable" (blocage/expiration du délai) ), le strict maxsize
obeyance ou le "Ignorez ces méthodes (_put, _get, ..) pour implémenter d'autres organisations de files d'attente" comportement de sous-classification, ou lorsque vous vous en occupez alors vous-même, un deque
nu est une bonne et efficace affaire pour une communication inter-thread très rapide.
En fait, l'utilisation intensive d'un mutex supplémentaire et d'une méthode supplémentaire ._get()
etc., appels de méthode dans Queue.py
Est due à des contraintes de compatibilité avec les versions antérieures, à une sur-conception antérieure et au manque de soin pour fournir une solution efficace pour cet important problème de vitesse goulot d'étranglement dans la communication inter-thread. Une liste était utilisée dans les anciennes Python - mais même list.append () /. Pop (0) était & is atomic et threadsafe ...
deque 0.469802
Queue 0.667279
@ Jonathan modifie un peu son code et j'obtiens le point de repère à l'aide de cPython 3.6.2 et ajoute une condition dans une boucle deque pour simuler le comportement de la file d'attente.
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)
Et il semble que les performances limitées par cette fonction condition.notify_all()
collections.deque est une implémentation alternative des files d'attente non limitées avec des opérations atomiques rapides append () et popleft () qui ne nécessitent pas de verrouillage. file d'attente de la documentation
(Il semble que je n'ai pas de réputation à commenter ...) Vous devez faire attention aux méthodes de la deque que vous utilisez depuis différents threads.
deque.get () semble être threadsafe, mais j'ai trouvé que faire
for item in a_deque:
process(item)
peut échouer si un autre thread ajoute des éléments en même temps. J'ai eu un RuntimeException qui s'est plaint "deque muté pendant l'itération".
Vérifiez collectionsmodule.c pour voir quelles opérations sont affectées par cela
deque
est thread-safe. "opérations qui ne nécessitent pas de verrouillage" signifie que vous n'avez pas à verrouiller vous-même, le deque
s'en charge.
En regardant la source Queue
, le deque interne s'appelle self.queue
Et utilise un mutex pour les accesseurs et les mutations, donc Queue().queue
est not thread-safe à utiliser.
Si vous recherchez un opérateur "in", une deque ou une file d'attente n'est peut-être pas la structure de données la mieux adaptée à votre problème.