226

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

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

但是,队列文档还指出:

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

我想我不太明白:这是否意味着双端队列毕竟不是完全线程安全的?

如果是这样,我可能无法完全理解这两个类之间的区别。我可以看到 Queue 添加了阻塞功能。另一方面,它失去了一些双端队列特性,比如对 in-operator 的支持。

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

x in Queue().deque

线程安全?

另外,当双端队列已经是线程安全的时,为什么队列要为其操作使用互斥锁?

4

7 回答 7

347

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

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

最后,访问和操作 a 的内部双端队列Queue.Queue是在玩火——你真的不想这样做。

于 2009-04-04T15:26:29.343 回答
58

如果您正在寻找的只是一种在线程之间传输对象的线程安全方式,那么两者都可以工作(对于 FIFO 和 LIFO)。对于先进先出:

笔记:

  • 其他操作deque可能不是线程安全的,我不确定。
  • deque不会阻塞,pop()或者popleft()您不能将您的消费者线程流基于阻塞,直到新项目到达。

但是,deque 似乎具有显着的效率优势。以下是使用 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
于 2013-12-02T14:20:14.690 回答
12

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

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

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

无论如何,如果您不是 100% 确定并且您更喜欢可靠性而不是性能,那么只需放一个 Lock ;)

于 2015-12-12T11:41:50.660 回答
10

所有单元素方法deque都是原子的和线程安全的。所有其他方法也是线程安全的。之类的东西会len(dq)产生dq[4]暂时的正确值。但是请考虑一下dq.extend(mylist):当其他线程也在同一侧附加元素时,您不能保证所有元素mylist都连续归档 - 但这通常不是线程间通信和被质疑任务的要求。

所以 adequeQueue(在引擎盖下使用 a deque)快约 20 倍,除非您不需要“舒适”同步 API(阻塞/超时)、严格maxsize遵守或“覆盖这些方法(_put、_get、.. ) 来实现其他队列组织的子类化行为,或者当你自己处理这些事情时,那么deque对于高速线程间通信来说,裸机是一个很好且有效的处理。

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

于 2017-04-19T16:08:31.030 回答
6

添加notify_all()到每个deque appendpopleft导致deque比默认行为实现的 20 倍改进deque更糟糕的结果:

deque + notify_all: 0.469802
Queue:              0.667279

@Jonathan 稍微修改了他的代码,我使用 cPython 3.6.2 获得了基准,并在 deque 循环中添加了条件来模拟 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() 操作。 文档队列

于 2017-12-25T18:07:35.950 回答
2

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

查看Queue源代码,内部双端队列被调用self.queue并使用互斥锁进行访问器和突变,因此使用起来不是Queue().queue线程安全的。

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

于 2009-04-04T14:42:23.863 回答
1

(似乎我没有评论的声誉......)您需要注意从不同线程使用的双端队列方法。

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

for item in a_deque:
   process(item)

如果另一个线程同时添加项目,则可能会失败。我得到了一个 RuntimeException,它抱怨“在迭代过程中双端队列发生了突变”。

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

于 2015-07-28T09:31:09.400 回答