Queue.Queue与collections.deque


181

我需要一个队列,多个线程可以将内容放入其中,并且多个线程可以读取。

Python至少有两个队列类,Queue.Queue和collections.deque,前者似乎在内部使用后者。两者都声称在文档中是线程安全的。

但是,队列文档还指出:

collections.deque是具有无限原子append()和popleft()操作的无界队列的替代实现,不需要锁定。

我猜我不太理解:这是否意味着双端队列毕竟不是完全线程安全的?

如果是这样,我可能无法完全理解两个类之间的区别。我可以看到Queue添加了阻止功能。另一方面,它失去了一些过时的功能,例如对操作员的支持。

直接访问内部双端队列对象是

Queue()中的x

线程安全的?

另外,当双端队列已经是线程安全的了,为什么Queue在操作上使用互斥锁?


RuntimeError: deque mutated during iteration您可能会得到的是使用deque多个线程之间的共享并且没有锁定...
toine 2015年

4
@toine与线程无关。每当deque在同一线程中添加/删除一段时间时,都可能会出现此错误。无法获得此错误的唯一原因QueueQueue不支持迭代。
最大

Answers:


280

Queue.Queuecollections.deque达到不同的目的。Queue.Queue旨在允许不同的线程使用排队的消息/数据进行通信,而collections.deque仅仅是作为数据结构。这就是为什么Queue.Queue有类似的方法put_nowait()get_nowait()join(),而collections.deque不会。Queue.Queue不打算用作集合,这就是为什么它缺少in运算符之类的原因。

归结为:如果您有多个线程,并且希望它们能够在不需要锁的情况下进行通信,那么您正在寻找Queue.Queue;如果您只想将队列或双端队列作为数据结构,请使用collections.deque

最后,访问和处理内部的双端队列 Queue.Queue正在玩火-您确实不想这样做。


6
不,那根本不是个好主意。如果您查看的来源Queue.Queue,它会deque在引擎盖下使用。collections.deque是一个集合,同时Queue.Queue是一个通信机制。开销Queue.Queue是使它成为线程安全的。使用deque线程间通信,只会导致痛苦的比赛。每当deque碰巧是线程安全的时,这就是解释器如何实现的不幸事件,而不是要依赖的东西。这就是为什么Queue.Queue首先存在。
基思·高恩

2
请记住,如果您跨线程通信,则使用双端队列(deque)会使您玩火。由于GIL的存在,deque 偶然是线程安全。不含GIL的实现将具有完全不同的性能特征,因此不建议其他实现。此外,您是否已将Queue vs deque定时用于跨线程使用,而不是在单个线程中使用天真的基准测试?如果你的代码要排队的速度VS双端队列敏感,Python的可能不是你要找的语言。
基思·高恩

3
@KeithGaughan deque is threadsafe by accident due to the existence of GIL; 确实deque依赖GIL来确保线程安全-但并非如此by accident。官方python文档明确指出deque pop*/ append*方法是线程安全的。因此,任何有效的python实现都必须提供相同的保证(无GIL的实现必须弄清楚如何在没有GIL的情况下做到这一点)。您可以放心地依靠这些保证。
最大

2
@fantabolous尽管我以前的评论,但我不太了解您将如何使用它deque进行交流。如果包装poptry/except,则会导致一个繁忙的循环,仅在等待新数据时便会吞噬大量CPU。与所提供的阻塞调用相比,这似乎是一种效率极低的方法,该方法Queue可确保等待数据的线程进入睡眠状态,并且不会浪费CPU时间。
最多

3
您可能需要阅读一下源代码Queue.Queue,因为它是使用collections.deque以下格式编写的:hg.python.org/cpython/file/2.7/Lib/Queue.py-它使用条件变量来有效地允许deque其包装被访问安全有效地在线程边界上运行。deque源代码中有关于如何使用a 进行通信的说明。
基思·高恩

44

如果您要寻找的是一种在线程之间传输对象的线程安全方法,那么两者都将起作用(对于FIFO和LIFO都适用)。对于FIFO:

注意:

  • 的其他操作 deque我不确定可能不是线程安全的。
  • deque并未阻挡pop()popleft()让你无法立足于阻塞,直到一个新项目到达您的消费者线程流。

但是,似乎双端队列具有明显的效率优势。这是使用CPython 2.7.3在几秒钟内插入和删除100k项的一些基准测试结果

deque 0.0747888759791
Queue 1.60079066852

这是基准代码:

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
您声称“对其进行的其他操作deque可能不是线程安全的”。你从哪里得到的?
马特

@Matt-改写以更好地传达我的意思
Jonathan

3
好,谢谢。那使我无法使用双端队列,因为我以为你知道我不知道的东西。我猜我只是假设它是线程安全的,直到我发现其他情况为止。
马特

@Matt“双端队列的append(),appendleft(),pop(),popleft()和len(d)操作在CPython中是线程安全的。” 来源:bugs.python.org/issue15329
Filippo Vitale,

7

有关信息,请参阅Python票证中的双端线程安全性(https://bugs.python.org/issue15329)。标题“阐明哪些双端队列方法是线程安全的”

底线在这里:https : //bugs.python.org/issue15329#msg199368

双端队列的append(),appendleft(),pop(),popleft()和len(d)操作在CPython中是线程安全的。append方法的末尾有一个DECREF(对于已设置maxlen的情况),但这会在所有结构更新完成并且不变量已恢复之后发生,因此可以将这些操作视为原子操作。

无论如何,如果您不确定100%的可靠性,而宁愿选择可靠性而不是性能,则只需放一个类似Lock的锁即可;)


6

所有启用的单元素方法deque都是原子和线程安全的。所有其他方法也是线程安全的。之类的东西len(dq)dq[4]产生瞬间的正确的价值观。但是想想一下dq.extend(mylist)mylist当其他线程也在同一侧附加元素时,您不能保证所有元素都连续提交,但这通常不是线程间通信和有问题的任务所必需的。

因此,a的deque速度要快20倍左右Queue(后者dequemaxsize幕后使用),除非您不需要“舒适的”同步API(阻止/超时),严格遵守或“覆盖这些方法(_put,_get,.. )来实现其他队列组织的子类化行为,或者当您自己处理此类事情时,光秃秃的deque是高速线程间通信的好方法。

实际上,大量使用额外的互斥锁和额外的方法._get()Queue.py是由于向后兼容性限制,过去的过度设计以及缺乏为线程间通信中这一重要的速度瓶颈问题提供有效解决方案的注意。在较旧的Python版本中使用了列表-但是list.append()/。pop(0)甚至是&都是原子和线程安全的...


3

默认行为的20倍改进相比notify_all()每个结果相加会导致更差的结果:deque appendpopleftdequedeque

deque + notify_all: 0.469802
Queue:              0.667279

@Jonathan稍微修改了他的代码,我使用cPython 3.6.2获得了基准,并在双端队列中添加了条件以模拟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)

而且似乎该功能限制了性能 condition.notify_all()

collections.deque是具有无限原子append()和popleft()操作的无界队列的替代实现,不需要锁定。 docs队列


2

deque是线程安全的。“不需要锁定的操作”意味着您不必自己进行锁定,deque多多关照。

以一看Queue源,内部双端队列被称为self.queue并使用存取和突变互斥,所以Queue().queue线程安全的使用。

如果您正在寻找“ in”运算符,则双端队列或队列可能不是最适合您问题的数据结构。


1
好吧,我要做的是确保没有重复项添加到队列中。这不是队列可能支持的东西吗?
miracle2k 2009年

1
最好有一个单独的集合,并在从队列中添加/删除某些内容时进行更新。这将是O(log n)而不是O(n),但是您必须小心保持集合和队列同步(即锁定)。
brian-巴西

Python集是一个哈希表,因此它将是O(1)。但是,是的,您仍然必须进行锁定。
AFoglia,2012年

1

(似乎我没有信誉可言...)您需要注意从不同线程使用的双端队列的哪些方法。

deque.get()似乎是线程安全的,但是我发现这样做

for item in a_deque:
   process(item)

如果另一个线程同时添加项目,则失败。我收到一个RuntimeException,它抱怨“迭代期间双端队列已变异”。

检查collectionsmodule.c以查看哪些操作受此影响


此类错误对于线程和主体线程安全而言并不特殊。它的发生例如只是通过执行 >>> di = {1:None} >>> for x in di: del di[x]
kxr

1
基本上,您永远不应循环访问可能被另一个线程修改的内容(尽管在某些情况下,您可以通过添加自己的保护来做到这一点)。像队列一样,您打算在处理项目之前将其弹出/从队列中取出,通常可以通过while循环来完成。
惊人的
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.