web-dev-qa-db-fra.com

Queue.Queue vs. collections.deque

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?

151
miracle2k

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.

244
Keith Gaughan

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:

  • D'autres opérations sur 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
38
Jonathan

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 à;

6
BadWolf

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 ...

4
kxr
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

3
nikan1996

(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

2

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.

2
brian-brazil