ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

为什么 Python 的 queue 模块里会有 LifoQueue?

为什么 Python 的 queue 模块里会有 LifoQueue? 为什么 Python 的queue模块里会有LifoQueue我一开始有一个疑问为什么 Python 的queue模块里会有一个LifoQueuequeue不是 FIFO 吗这个问题看起来像是个命名矛盾但当我顺着官方文档和 CPython 源码往下看才发现问题出在我把模块名当成了某一个类的语义。下面是我对这个问题的完整梳理。一、关键澄清queue是模块名不是 FIFO 的同义词在计算机科学里“queue”通常指先进先出FIFO的队列。但在 Python 标准库里queue是一个模块名而不是某一个具体类的语义。这个模块提供了一组线程安全的容器类它们共享几乎相同的 API区别仅在于元素的取出顺序。Python 官方文档对queue模块的定义非常明确本模块实现了三种类型的队列它们的区别仅仅是条目的提取顺序。在 FIFO 队列中先添加的任务会先被提取。在 LIFO 队列中最近添加的条目会先被提取类似于一个栈。在优先级队列中条目将保持已排序状态使用heapq模块并且值最小的条目会先被提取。换句话说queue模块的命名逻辑是功能导向的它为“需要在多线程之间安全传递数据”这一场景提供了一组容器。这些容器都支持put()和get()操作都具备阻塞能力都实现了必要的锁语义唯一的区别是元素的出队顺序。模块中定义了以下核心类类名出队顺序数据结构本质queue.QueueFIFO先进先出队列queue.LifoQueueLIFO后进先出栈queue.PriorityQueue按优先级值最小者先出堆queue.SimpleQueueFIFO无界简单队列Python 3.7此外还定义了两个异常queue.Empty对空队列调用非阻塞get时抛出和queue.Full对满队列调用非阻塞put时抛出。这种设计的合理性在于“线程安全的排队容器”这一需求远比“FIFO 队列”更宽泛。在多线程编程中我们有时需要按到达顺序处理任务FIFO有时需要“后到的先处理”LIFO例如撤销操作、深度优先搜索的迭代实现有时需要按优先级调度。把这些需求统一到一个模块下以一致的 API 暴露是一种务实且优雅的设计。二、LifoQueue的本质一个线程安全的栈2.1 行为示例LifoQueue的行为与栈完全一致。以下代码展示了它的基本用法importqueue qqueue.LifoQueue()q.put(1)q.put(2)q.put(3)print(q.get())# 3print(q.get())# 2print(q.get())# 1官方文档对此的描述是“LIFO 队列的构造函数。……最近添加的条目会先被提取类似于一个栈。”2.2 源码层面的实现LifoQueue并不是从头实现的一个新类而是Queue的子类仅通过覆写少数几个内部方法来实现 LIFO 语义。查看 CPython 源码中queue.py的结构可以看到Queue类已经定义了完整的锁机制、阻塞逻辑和任务跟踪而LifoQueue只需改动底层容器的操作方式。具体来说Queue类使用collections.deque作为底层存储get操作使用popleft()从而实现 FIFO。LifoQueue覆写了_init()将底层容器改为list_put()使用list.append()_get()使用list.pop()从而实现 LIFO。PriorityQueue同样覆写_init()使用list并借助heapq模块维护堆序。这种设计使得LifoQueue几乎“免费”获得了Queue的全部线程安全特性——包括互斥锁、条件变量、join()/task_done()任务跟踪等。2.3 为什么叫LifoQueue而不是Stack这是一个合理的命名问题。严格来说LifoQueue就是一个栈。但 Python 选择LifoQueue这个名称主要是为了保持模块内命名的统一性。queue模块中的所有类都以Queue结尾暗示它们共享相同的“put/get 容器”接口。如果叫Stack反而会割裂它与Queue在 API 上的一致性。此外“LIFO 队列”这个说法强调的是排队策略而非数据结构本身。从这个角度看LifoQueue的命名是自洽的它是一个按 LIFO 规则出队的队列。三、线程安全机制LifoQueue真正区别于普通栈的地方如果只是需要一个栈Python 程序员通常会直接使用list的append()/pop()或者collections.deque。那么LifoQueue存在的意义是什么答案是线程安全。queue模块的定位是“多生产者-多消费者队列”它特别适用于多线程编程中信息必须在多个线程之间安全交换的场景。模块中的Queue类实现了所有必需的锁语义。LifoQueue继承了这套机制。它在内部使用threading.Lock作为互斥锁配合多个threading.Condition对象来实现阻塞和通知not_empty当队列为空时等待get()的线程会在此条件变量上阻塞当put()添加元素后会通知该条件变量。not_full当队列已满maxsize 0时等待put()的线程会在此条件变量上阻塞当get()取出元素后会通知该条件变量。all_tasks_done配合join()和task_done()实现任务完成跟踪。这意味着LifoQueue的put()和get()是原子操作。多个线程可以同时向同一个LifoQueue放入或取出元素而不会出现数据竞争或状态损坏。相比之下list的append()/pop()虽然在 CPython 中由于 GIL 的存在单个操作通常表现为原子操作但这是实现细节且组合操作如“检查是否为空再弹出”并不安全。以下是一个多线程场景下的LifoQueue使用示例importqueueimportthreading qqueue.LifoQueue(maxsize5)defproducer():foriinrange(5):q.put(i)print(fProduced:{i})defconsumer():for_inrange(5):itemq.get()print(fConsumed:{item})q.task_done()t1threading.Thread(targetproducer)t2threading.Thread(targetconsumer)t1.start()t2.start()t1.join()t2.join()q.join()在这个例子中LifoQueue保证了生产者和消费者之间的安全协调当队列满时put()会阻塞当队列空时get()会阻塞。由于是 LIFO消费者取出的顺序通常是后放入的元素先被取出。四、queue.LifoQueuevs.collections.dequevs.list既然collections.deque也可以作为栈使用append()/pop()并且性能更好为什么还需要LifoQueue维度queue.LifoQueuecollections.dequelist线程安全完全线程安全内置锁单个 append/pop 在 CPython 中通常原子但组合操作不安全同上阻塞能力支持get()可阻塞等待不支持不支持任务跟踪支持join()/task_done()不支持不支持性能较慢Python 层锁开销快C 实现快C 实现适用场景多线程通信单线程数据结构单线程数据结构官方文档也指出Queue.Queue的目的是允许不同的线程使用排队的消息/数据进行通信而collections.deque只是作为一种数据结构。因此Queue.Queue有put_nowait()、get_nowait()和join()等方法而collections.deque没有。deque不会在pop()或popleft()上阻塞因此在新项目到达之前消费者线程不能基于阻塞来组织流程。但deque具有显著的效率优势因为它是 C 实现的而Queue涉及 Python 层的锁和条件变量。因此选择规则可以概括为多线程通信、需要阻塞或任务跟踪使用queue.LifoQueue单线程、纯数据结构使用collections.deque更推荐或list五、LifoQueue的适用场景LifoQueue在实际开发中有一些典型的应用场景撤销/重做系统用户操作通常需要按“最后操作最先撤销”的顺序处理LIFO 语义天然匹配。深度优先搜索DFS的迭代实现在需要显式维护栈的 DFS 算法中如果涉及多线程任务调度可以用LifoQueue替代手动管理的栈。任务回滚当一系列操作失败时需要按相反顺序回滚LIFO 队列可以自然地维护回滚顺序。表达式求值在解释器或计算器的实现中操作数栈可以用LifoQueue来管理同时享受线程安全保证。需要注意的是在单线程场景下使用LifoQueue通常是不必要的。它的锁开销会带来性能损失而这种损失在没有多线程竞争时毫无收益。单线程栈应优先考虑list或collections.deque。六、queue模块的家族全景为了完整理解LifoQueue的位置有必要简要梳理 Python 中队列相关的模块生态模块队列类适用并发模型线程/进程安全性queueQueue、LifoQueue、PriorityQueue多线程线程安全asyncioasyncio.Queue、asyncio.LifoQueue、asyncio.PriorityQueue协程异步 I/O协程安全非线程安全multiprocessingmultiprocessing.Queue多进程进程安全collectionsdeque单线程单个操作在 CPython 中通常原子组合操作不安全queue.Queue适用于多线程场景asyncio.Queue适用于协程场景下的通信multiprocessing.Queue适用于多进程场景。这三者的 API 设计高度相似但底层同步机制完全不同queue使用线程锁asyncio使用事件循环和协程调度multiprocessing使用进程间通信管道 锁。值得注意的是asyncio模块也提供了LifoQueue与queue.LifoQueue在语义上完全对应只是面向的并发模型不同。七、总结回到我最初的疑问为什么 Python 的queue模块里有LifoQueue因为queue不是“FIFO 队列”的同义词而是一个线程安全队列容器的命名空间。它封装了三种不同的排队策略——FIFO、LIFO 和优先级——并为其附加了统一的线程安全保证。LifoQueue正是其中 LIFO 策略的实现本质上是一个带有锁和条件变量的线程安全栈。它的存在填补了一个重要的空白在多线程编程中我们有时需要的不是 FIFO 队列而是 LIFO 的“后进先出”处理顺序同时还需要线程安全、阻塞能力和任务跟踪。list和collections.deque虽然可以作为栈使用但它们不提供阻塞和任务跟踪asyncio.LifoQueue面向的是协程而非线程。queue.LifoQueue恰好服务于“多线程 LIFO 安全通信”这一特定但真实的需求。理解这一点也就理解了 Python 标准库的一个设计原则以模块为单位组织相关功能以统一的接口暴露不同的策略让使用者根据场景选择而非被模块名的字面含义所局限。
返回列表