python的 collections.deque 和 queue.Queue 都是用于同一进程中实现队列(FIFO)的数据结构,但它们的设计目标、实现机制和适用场景有显著区别。以下是详细对比:
| 特性 | collections.deque | queue.Queue |
| *模块/目的 | 通用数据结构容器 | 多线程间安全通信 |
| 线程安全性 | 非线程安全 | 线程安全(基于锁机制) |
| 阻塞操作 | 不支持 (空取抛异常) | 支持, get()/put()阻塞等待 |
| 性能 | 更快 | 较慢(锁开销) |
| 内存布局 | 双向链表块 | 基于 `deque` 的封装 |
| 最佳场景 | 单线程高性能栈/队列操作、单生产者-单消费者模式 | 多生产者-多消费者多线程模式 |
性能对比:
collections.deque:O(1) 时间复杂度的两端插入和删除,速度非常快。
queue.Queue:在存取数据前后,它必须获取互斥锁并通知条件变量。这带来了额外的性能开销。速度比collections.deque慢。
线程安全性对比:
queue.Queue:专门为多线程设计,尤其是多生产者-多消费者的多线程模式。由于有锁开销,速度比collections.deque慢,可能会造成丢数据,即有些数据没有被put()进去,因为在存数据/取数据前,它必须获取互斥锁并通知条件变量。如果队列空了,调用 get() 的线程会自动阻塞(休眠),直到有其他线程 put() 数据进去。如果设置了 maxsize 导致队列满了,调用 put() 的线程也会阻塞,直到有空间。
collections.deque:它的 append() 和 popleft() 操作在字节码层面是原子的,所以在多线程中简单使用是安全的(单生产者-单消费者模式下不会崩溃)。但是,它没有“等待”机制。如果你试图从空的 deque 中 pop,它会直接报错 (IndexError)。你必须自己写代码去轮询或加锁来处理同步问题。如果只有一个线程append(),另一个线程popleft(),这样用collections.deque会大幅度提高存取速度。在进行popleft()前,要进行判空处理,否则会抛异常。但是需要注意以下内容:
在 collections.deque 中,一个线程 append()(写),另一个线程 popleft()(读),虽然操作本身是“线程安全”的(不会导致 Python 崩溃),但在业务逻辑和性能上,你必须时刻注意以下 3 个核心风险:
1. 并没有“阻塞等待”机制(最重要的问题)
这是与 queue.Queue 最大的区别。
- 现象:如果消费者(
popleft)的速度快于生产者(append),队列会变空。 - 后果:当队列为空时,调用
popleft()会直接抛出IndexError: pop from an empty deque。 - 你需要做的:你必须在代码中捕获这个异常,或者先检查长度。
2. "Check-then-Act" 竞态条件(Race Condition)
虽然 append 和 popleft 单独看都是原子的(Atomic),但组合操作不是。
错误的写法(高危):
if len(d) > 0: # 步骤 1: Check
# <--- 如果在这里发生了线程切换,或者有第三个线程插入/删除了数据
val = d.popleft() # 步骤 2: Act -> 可能报错!
虽然在你描述的“单生产者-单消费者”场景下,Python 的 GIL 可能会让上述代码“侥幸”正常工作,但这是极不规范的写法。一旦有人增加了第二个消费者线程,这段代码就会立即产生 Bug。
正确的写法(使用 EAFP 准则):
try:
val = d.popleft()
except IndexError:
# 队列是空的,处理空的情况
val = None
3. "忙等待" (Busy Waiting) 导致的 CPU 飙升
这是新手最容易踩的坑。由于 popleft() 不会阻塞,为了等待数据,消费者线程通常会写在一个 while True 循环里。
- 场景:队列空了。
- 代码行为:消费者线程会疯狂地执行
try...except,每秒钟可能执行几百万次空循环。 - 后果:CPU 占用率瞬间飙升至 100%(单核),浪费系统资源,甚至拖慢生产者线程。
反例(CPU 杀手):
# ❌ 千万别这么写
while True:
try:
item = d.popleft()
process(item)
except IndexError:
pass # 这里的 pass 会导致死循环空转
解决方案:
如果你坚持不使用 queue.Queue,你必须在捕获异常手动让线程 sleep 一会儿:
import time
# ✅ 勉强可用的方案
while True:
try:
item = d.popleft()
process(item)
except IndexError:
time.sleep(0.01) # 虽然只有 0.01秒,但能极大释放 CPU
总结建议
虽然用 collections.deque 实现“一写一读”在技术上是可行的(依靠 GIL 的原子性),但你需要自己处理:
- 异常捕获 (
IndexError)。 - 等待策略 (
time.sleep) 以防止 CPU 空转。
结论:
如果单生产者-单消费者模式,使用collections.deque,速度快。如果多生产者-多消费者模式,使用queue.Queue,因为线程安全。除非你要做极高性能的调优(且非常清楚 GIL 的行为),否则请直接使用 queue.Queue。它内部封装的条件变量机制,可以让线程在队列空时真正挂起(不占用 CPU),并在有数据进入时被操作系统立刻唤醒,这比你自己写 sleep 要高效且优雅得多。但是用queue.Queue时,可能会丢包,是因为在存取数据前后,它必须获取互斥锁并通知条件变量,这带来了额外的性能开销。
本质:queue.Queue 本质是给 collections.deque 加上了线程同步层。
注意:如果在做多进程 (Multiprocessing) 通信,应该使用 multiprocessing.Queue,因为进程间内存是不共享的,上述两个队列都无法跨进程工作。
275

被折叠的 条评论
为什么被折叠?



