deque 不是无锁的,多线程下必须加锁;其 maxlen 参数可实现固定容量环形行为,但仅 append/appendleft 触发自动丢弃,且不可动态修改。

deque 本身不是无锁的,别被名字误导
collections.deque 在 CPython 中对单端操作(如 append()、popleft())是原子的,但不保证多线程下的无锁语义。它的内部使用双向链表 + 块数组混合结构,部分操作(如满容时扩容、跨块索引)会涉及内存重分配或临界区竞争。真正“无锁”需依赖底层 CAS 或 lock-free 算法,而 deque 没有提供这类保证。
- 多线程下直接用
deque当环形队列,可能在append()和popleft()同时触发内存调整时引发RuntimeError: deque mutated during iteration或静默数据错乱 - 它也不限制长度:默认无限增长,要“环形”,必须手动丢弃旧元素或检查容量
用 maxlen 实现固定容量环形行为
deque 的 maxlen 参数是唯一接近环形语义的内置机制:当添加新元素超过容量时,自动从对端挤出最老元素。
from collections import deque <h1>容量为 4 的环形缓冲区</h1><p>ring = deque(maxlen=4) ring.extend([1, 2, 3]) print(list(ring)) # [1, 2, 3]</p><p>ring.append(4) print(list(ring)) # [1, 2, 3, 4]</p><p>ring.append(5) # 挤出 1 print(list(ring)) # [2, 3, 4, 5]</p>
-
maxlen是构造时指定的硬限制,不可动态修改 - 挤出行为只发生在
append()/appendleft(),pop()/popleft()不触发补位 - 若需“写满即覆盖+读取不阻塞”,这已足够;但若需严格 FIFO + 读写并发安全,仍得加锁
多线程下必须加锁,别省那几行代码
即使用了 maxlen,只要存在多个线程同时调用 append() 和 popleft(),就可能因内部状态不一致导致异常(例如两个线程同时触发块切换)。
- 常见错误现象:
IndexError: pop from an empty deque,尽管你确认刚 append 过 —— 因为append()和popleft()都非完全可重入 - 正确做法:用
threading.Lock包裹所有变更操作 - 示例(带超时的非阻塞读):
import threading
from collections import deque
<p>class LockedRingBuffer:
def <strong>init</strong>(self, maxlen):
self._buf = deque(maxlen=maxlen)
self._lock = threading.Lock()</p><pre class='brush:python;toolbar:false;'>def push(self, item):
with self._lock:
self._buf.append(item)
def try_pop(self):
with self._lock:
return self._buf.popleft() if self._buf else None- 不要用
RLock:这里没有递归调用需求,且RLock带来额外开销和死锁风险 - 别试图用
queue.Queue替代:它虽线程安全,但不是环形,也无自动丢弃机制
真要无锁?换专用库或自己实现 CAS 原语
CPython 的 GIL 不能替代并发安全,更不等于无锁。若场景要求高吞吐、低延迟、多核并行(比如实时音频缓冲、高频 tick 数据流),deque 就不合适了。
立即学习“Python免费学习笔记(深入)”;
- 可选方案:
array.array+threading.Semaphore手动管理头尾指针(仍是锁,但更轻量) - 更进一步:用
cython或rust-cpython实现基于原子整数的 ring buffer,暴露 CAS 接口 - 现成轮子:
ringbuffer(PyPI)或circus库,但多数仍用锁封装,标称“lock-free”者往往只在单生产者/单消费者(SPSC)模式下成立
环形队列的“无锁”从来不是靠容器选型决定的,而是由访问模式、内存模型和同步原语共同约束。用 deque 加 maxlen 快速验证逻辑可以,上线前务必确认线程模型与丢弃策略是否真匹配。


















