高天析Python任务调度 队列以原生阻塞防范多线程竞争崩溃

【python】queue是个啥?能干啥?啥是任务分配? — 码农高天

码农高天

用内置列表替代队列会在多线程下引发CPU空轮询与竞态崩溃,开发者必须依托原生队列的线程安全特性与task_done计数协同,才能实现无死锁的动态负载均衡与平稳退出。

AI-assisted summary of 【python】queue是个啥?能干啥?啥是任务分配? — original source. Check the original for full context.

Source published:

Key ideas

  • Queue(队列)通过先进先出(FIFO)机制确保任务调度的公平性与有序性,其基础操作 put 和 get 的时间复杂度均为 O(1)。 (0:00)
  • 相较于标准 Queue,使用内置 list 模拟队列存在 list.pop(0) 带来的 O(n) 内存挪动开销,且在多线程下极易引发竞态条件导致程序崩溃。 (1:45)
  • 在生产者-消费者模型中,基于队列的“领任务”模式能够根据任务实际耗时自然实现工作线程间的动态负载均衡。 (4:10)
  • 标准 Queue 具备原生阻塞挂起特性,结合 queue.task_done() 与 queue.join() 可以精确实现多线程任务的协同完成与优雅退出。 (7:44)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 11 月 3 日发布。视频简介指出,本期内容深入探讨了 Queue(队列)数据结构及其在 Python 中的应用场景,并系统讲解了 Producer-Consumer(生产者-消费者)模型、使用队列的核心原因及其架构优势。

## TL;DR 视频从生活中的排队买票隐喻切入,解析了先进先出(FIFO)队列的基础特性,并通过二叉树广度优先搜索(BFS)阐明其保障时序的核心价值。高天从 O(n) 出队耗时、CPU 空轮询损耗以及多线程竞态条件(Race Condition)三个维度,透彻解释了为何不能用 Python 内置的 list 代替队列。随后,视频重点演示了生产者-消费者模型如何借助阻塞队列实现天然的动态负载均衡与解耦,并针对多线程退出的工程陷阱,深度剖析了 queue.task_done() 与 queue.join() 的协同计数机制。

## Atmosphere & style 讲解风格幽默平实且极接地气,高天善用“人体大肠”、“窗口排队”、“各科老师留作业”等生活隐喻化解复杂的并发逻辑。视频推进节奏紧凑、直面工程痛点,从简单的功能调用逐步深入到多线程并发陷阱与底层原语,兼具轻松生动的视听趣味与严谨硬核的编码实战感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=0s) Queue(队列)通过先进先出(FIFO)机制确保任务调度的公平性与有序性,其基础操作 put 和 get 的时间复杂度均为 O(1)。 - [01:45](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=105s) 相较于标准 Queue,使用内置 list 模拟队列存在 list.pop(0) 带来的 O(n) 内存挪动开销,且在多线程下极易引发竞态条件导致程序崩溃。 - [04:10](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=250s) 在生产者-消费者模型中,基于队列的“领任务”模式能够根据任务实际耗时自然实现工作线程间的动态负载均衡。 - [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) 标准 Queue 具备原生阻塞挂起特性,结合 queue.task_done() 与 queue.join() 可以精确实现多线程任务的协同完成与优雅退出。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=0s) 队列(Queue)最基础的形式是先进先出(FIFO)的线性结构,核心功能仅包含 put(放)与 get(拿)。 - [00:00](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=0s) 生活中的排队隐喻说明了队列能避免无序争抢、防止不公平现象,并支持根据负载灵活增减服务窗口。 - [01:45](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=105s) 在二叉树广度优先搜索(BFS)等经典算法中,队列的有序性是确保层序遍历逻辑正确、不退化为深度优先搜索(DFS)的基石。 - [01:45](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=105s) 不能用 Python 的内置 list 代替标准 Queue,因为 list.pop(0) 具有 O(n) 的时间复杂度,存在明显的性能缺陷。 - [04:10](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=250s) 生产者-消费者模型通过队列实现生产方与消费方的完全解耦,使双方无需关心对方的具体身份或数量匹配。 - [04:10](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=250s) 多线程消费者并发执行时只能保证“开始执行”的先后顺序,任务完成顺序取决于具体计算时长,从而自然实现了动态负载均衡。 - [06:05](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=365s) Queue 的 get() 默认具有阻塞特性,队列为空时消费者线程会自动挂起,避免了 list 空轮询造成的 CPU 资源浪费。 - [06:05](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=365s) 多线程下使用 list 的 if-pop 模式存在竞态条件(Race Condition),线程上下文切换极易引发 IndexError,而 Queue 自带线程安全保证。 - [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) 将消费者简单设为守护线程(daemon=True)会导致主线程执行完后未完成的任务被直接强制杀死。 - [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) 仅检测队列为空无法判断所有任务是否执行完毕,因为任务被消费者认领出队后仍可能处于耗时计算中。 - [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) Python 的 Queue 提供了内置任务计数机制:put() 会自增计数,消费者处理完毕调用 task_done() 递减计数,主线程调用 join() 阻塞等待归零以实现安全退出。 - [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) 标准 FIFO 队列是并发编程的核心基石,其机制可扩展延伸至 LifoQueue、PriorityQueue 及非阻塞调用等高级变体。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=0s) 队列的基本概念与生活中的排队隐喻

队列(Queue)是一种非常简单的线性有序数据结构。通常情况下,人们聊到 Queue 时指的都是先进先出(FIFO, First In, First Out)模型,即最先放进去的数据最先被拿出来;而在广义层面上,它也可以指能够逐个存入数据、并按照特定顺序输出的结构(例如 PriorityQueue 优先队列)。一个最基础的 Queue 核心只需要支持两个功能:“放”(put)与“拿”(get)。以 Python 内置库的队列为例,将 0 到 9 依序放进队列后,在队列非空的情况下逐个取出并打印,输出的顺序必然是 0 到 9。高天形象地打了一个生动的比方:“大家可以把它想象成人的大肠儿,有一边进东西,有点出东西。”

Queue 最常见的现实映射是有序安排任务。如果火车站或迪士尼的售票窗口没有队伍,人们来一个人就直接冲到窗口抢着买票,就会产生诸多混乱:售票窗口挤作一团导致整体效率大幅下降,而身材瘦小的人可能一辈子都买不到票,因为全凭谁强壮、谁嗓门大谁先买。因此人类发明了排队: - 大家按照到达的先后顺序排成一队,依次走到窗口办理; - 如果队伍排得过长,管理员可以多开几个售票窗口; - 如果队伍没人了,可以适当减少开放的窗口数量; - 这彻底避免了“来得早却始终买不到票”的不公平情况。

```mermaid flowchart LR accTitle: 队列基础 FIFO 模型 accDescr: 展示元素从队尾进入、在队列中按序排列、并从队头取出的先进先出流程

subgraph Queue["Queue (FIFO 队列)"] direction LR InTail["队尾 (Put)"] Node0["0"] Node1["1"] Node9["... 9"] OutHead["队头 (Get)"] InTail --> Node9 --> Node1 --> Node0 --> OutHead end

Producer["数据生产者"] -->|"put(item)"| InTail OutHead -->|"get() -> 0, 1,... 9"| Consumer["数据消费者"] ```

---

[01:45](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=105s) 算法应用与为什么不用内置 List

在编程实践中,队列的有序性往往是算法正确性的基石。以广度优先搜索(BFS)遍历二叉树为例:二叉树的每个节点包含数据,并分别拥有左、右子节点。实现宽度优先遍历时,最标准的做法是维护一个记录“接下来该搜索谁”的队列。每访问一个节点,先处理自己,再将其左孩子、右孩子按顺序入队,这样便天然保证了逐层向外的遍历顺序。高天指出,如果没有队列来强制保障有序性,逻辑就会失控甚至沦为深度优先搜索(DFS):“这根节点说我完事了,左孩子你先来,结果左孩子说我还有孩子呢,我孩子得来啊,这就变成深度搜索了。”凭借 FIFO 的保证,节点最终才能严格按照 1、2、3、4、5 这种层序宽度被依次访问。

不少人会疑惑,Python 中拥有极其灵活的内置 `list`,既然 `list` 能取出任意索引的数据,为何不直接用 `list.pop(0)`?高天解释说,用 `list` 在逻辑层面看似能跑通,但存在两个严重缺陷: 1. **时间复杂度差异(性能问题)**:对于标准 Queue 来说,`put` 和 `get` 操作通常都是 $O(1)$ 常数时间的。而 Python 的 `list` 在内存中是连续数组,执行 `list.pop(0)` 时需要将首个元素弹出,并把后续所有元素全部向前挪动一位,因而其时间复杂度高达 $O(n)$。 2. **多线程下的阻塞与线程安全机制缺失**:`list` 无法原生支持线程间的挂起等待与安全并发。

---

[04:10](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=250s) 生产者-消费者模型与负载均衡

谈到队列的高级用途,核心便是“生产者-消费者模型”(Producer-Consumer Model)。该模型的逻辑非常质朴:现实中存在任务的产生者(Producer),也存在任务的完成者(Consumer)。高天举例说,在学校里,化学老师、数学老师和物理老师留家庭作业,他们就是 Producer;布置下来的作业由你负责数学、同桌负责物理和化学,你们把作业凑齐交付,你们就是 Consumer。在实际系统架构中,生产者与消费者的数量比例千差万别,可能是 100 比 1,也可能是 1 比 100。引入队列后,任务两端完全实现了解耦:生产方只管往队列里塞任务,无需寻找由哪个具体的消费者干活;消费方只管从队列里拿任务,不必关心任务出自何人之手。

```mermaid flowchart TD accTitle: 生产者消费者与负载均衡架构 accDescr: 多个生产者向队列投递任务,由阻塞队列统一缓冲,多个工作线程动态争抢任务以实现负载均衡

subgraph Producers["生产者 (Producers)"] P1["Producer 1 (如: 数学老师)"] P2["Producer 2 (如: 物理老师)"] end

subgraph Buffer["线程安全队列 (Queue)"] Q["FIFO Task Queue<br/>(支持阻塞 get 与线程安全)"] end

subgraph Consumers["消费者 (Consumers)"] C1["Consumer 1 (工作线程 A)"] C2["Consumer 2 (工作线程 B)"] end

P1 -->|"put(task)"| Q P2 -->|"put(task)"| Q Q -->|"阻塞抢占 get()"| C1 Q -->|"阻塞抢占 get()"| C2 ```

当设定一个生产者往队列填充数字 0 到 9、由两个消费者线程并发读取并打印时,输出的数字不再是绝对递增的。这是因为队列只能保证任务“被开始执行”的先后顺序,无法左右任务“被执行完成”的时长。如果把 10 个任务机械地按数量各分 5 个给两个人,极易发生一人分到简单任务早早收工、另一人面对极其复杂的任务苦不堪言的局面。而让消费者从队列中“做完一个再领下一个”,便自然达成了动态负载均衡(Load Balancing)。

---

[06:05](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=365s) 阻塞特性与 List 轮询的线程安全隐患

Queue 的一个杀手级特性是其 `get()` 方法默认属于**阻塞操作(Blocking Operation)**。当队列为空时,调用 `get()` 的线程会自动挂起停滞,直到队列中有新元素被写入才被唤醒,“有任务就做,没有任务就歇着”。如果试图用 `list` 模拟这一流程,通常会写一个 `while True` 配合 `if my_list: my_list.pop(0)`。这种做法问题重重: - **空轮询浪费 CPU**:如果列表为空,CPU 会陷入无意义的飞速空转。 - **`time.sleep()` 难以量化**:休眠时间设短了依然白白浪费 CPU 计算资源;休眠时间设长了,突发的大量任务产生堆积,系统响应迟钝,远不如操作系统底层的事件阻塞与条件变量灵活。 - **缺乏线程安全(Race Condition)**:在单线程环境下,先判断 `if my_list:` 再执行 `my_list.pop(0)` 没有任何逻辑瑕疵。但在多线程并发环境下,线程 A 执行完非空判断后发生上下文切换,线程 B 也恰好通过了非空判断。此时如果列表中仅剩 1 个元素,两线程前后脚抢着执行 `pop(0)`,必定会导致后执行的线程在空列表上执行弹出操作,直接抛出 `IndexError` 异常崩掉程序。若要在 `list` 上解决这个问题,必须手动额外加锁(Locking),代码复杂度和心智负担倍增。

---

[07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) 线程退出陷阱与 `task_done` 协同机制

在多线程中使用队列常会遭遇控制退出的难题。直接运行两个 Consumer 线程时,主程序往往会卡住无法退出,必须手动按 `Ctrl+C` 强行终止。原因在于 Producer 投递完任务后函数已退出,不再产生新数据,但两个 Consumer 线程仍在空队列上无限阻塞等待,且 Python 默认主进程会等待所有非守护线程执行完毕才退出。

有开发者可能会尝试将线程的属性配置为 `daemon=True`(守护线程)。“Daemon 等于 True 的意思是只要主线程结束了我就结束。”然而一旦加上守护线程属性,程序运行后屏幕上可能空空如也:Producer 闪电般执行完毕后主线程随即终结,并强制将两个未及执行的子线程一并杀死。此外,仅在主线程监测“队列是否为空”也是错误的——当消费者线程把最后一个任务从队列拿出来时,队列瞬间变成了空,但该任务很可能还在被密集计算、尚未真正执行完成。“Queue 里面空只代表所有的任务都被认领了,不代表所有的任务都被完成了。”

针对这个痛点,Python 的 `queue.Queue` 提供了内置的协同计数机制: - 每当调用 `queue.put()` 往队列放入一个任务时,队列内部的计数器就会自增 1; - 消费者完整处理完一项任务后,必须显式调用一次 `queue.task_done()`,通知队列内部计数器减 1; - 主线程通过调用 `queue.join()` 实施阻塞,程序将静止停在这一行,直到所有被 `put()` 的任务都被消费者调用了对应的 `task_done()`、未完成计数归零后,`join()` 才会顺利放行并平稳退出。

高天总结道,这就是 Python 最经典标准的生产者-消费者模型。除基础 Queue 之外,Python 还提供了带超时的非阻塞 `put_nowait()` / `get_nowait()`、后进先出的 `LifoQueue`(栈行为)以及按优先级提取的 `PriorityQueue`,掌握最底层的 FIFO 队列与并发配合是理解其他所有高级变体的基石。

## Notable quotes > [07:44](https://www.youtube.com/watch?v=Qsa3xZgDUh4&t=464s) Queue 里面空只代表所有的任务都被认领了,不代表所有的任务都被完成了。 — 高天

## Mentioned 高天, Python, Queue, FIFO, PriorityQueue, LifoQueue, 生产者-消费者模型, 广度优先搜索, 深度优先搜索

## Video description 这期视频聊聊queue这个数据结构和python上的应用场景。也讨论了一下producer-consumer模型。尽可能细致地给大家讲解了一下为什么要用queue,以及它有什么好处。

[Source](https://www.youtube.com/watch?v=Qsa3xZgDUh4)

中文简报

高天析Python任务调度:队列以原生阻塞防范多线程竞争崩溃 用内置列表替代队列会在多线程下引发CPU空轮询与竞态崩溃,开发者必须依托原生队列的线程安全特性与task_done计数协同,才能实现无死锁的动态负载均衡与平稳退出。 视频从生活中的排队买票隐喻切入,解析了先进先出(FIFO)队列的基础特性,并通过二叉树广度优先搜索(BFS)阐明其保障时序的核心价值。高天从 O(n) 出队耗时、CPU 空轮询损耗以及多线程竞态条件(Race Condition)三个维度,透彻解释了为何不能用 Python 内置的 list 代替队列。随后,视频重点演示了生产者-消费者模型如何借助阻塞队列实现天然的动态负载均衡与解耦,并针对多线程退出的工程陷阱,深度剖析了 queue.task_done() 与 queue.join() 的协同计数机制。 Queue(队列)通过先进先出(FIFO)机制确保任务调度的公平性与有序性,其基础操作 put 和 get 的时间复杂度均为 O(1)。 (0:00) 相较于标准 Queue,使用内置 list 模拟队列存在 list.pop(0) 带来的 O(n) 内存挪动开销,且在多线程下极易引发竞态条件导致程序崩溃。 (1:45) 在生产者-消费者模型中,基于队列的“领任务”模式能够根据任务实际耗时自然实现工作线程间的动态负载均衡。 (4:10) 标准 Queue 具备原生阻塞挂起特性,结合 queue.task_done() 与 queue.join() 可以精确实现多线程任务的协同完成与优雅退出。 (7:44)

Reading summary

码农高天在视频中解析了队列(Queue)作为基础线性数据结构的核心价值,指出其不仅保障了先进先出(FIFO)与算法层面的有序性,更是实现高并发生产者-消费者模型、规避空轮询与数据竞争并达成动态负载均衡的基石。 队列的基础定义与现实映射 (0:00) 队列是一种支持逐个存入(put)和按特定顺序取出(get)的线性有序数据结构,最常见的形态遵循先进先出(FIFO)规则。高天将其形象地比喻为人体大肠“一边进、一边出”。在现实生活中,火车站或游乐园排队机制映射了这一逻辑:如果不排队而任由人群冲向售票窗口,整体效率会因拥挤大幅下降,且会出现体格瘦小者因争抢不过而长期买不到票的不公平情况。排队机制通过规范到达顺序,并允许管理员按队伍长短灵活增减服务窗口,兼顾了公平与秩序。 算法顺序约束与内置列表的缺陷 (1:45) 在算法设计中,队列的严格有序性是逻辑正确的基础。例如在二叉树的广度优先搜索(BFS)中,若缺乏队列维持层级顺序,节点遍历逻辑就会失控演变为深度优先搜索(DFS)。许多开发者常考虑使用 Python 内置的 `list` 配合 `list.pop(0)` 来替代队列,但高天指出这种做法存在显著弊端:标准队列的存取时间复杂度通常为 $O(1)$,而 `list` 基于连续数组内存结构,弹出首个元素会导致后续所有元素向前平移,时间复杂度高达 $O(n)$。此外,`list` 无法原生支持多线程下的挂起等待与安全并发机制。 生产者-消费者模型与动态负载均衡 (4:10) 队列的高级实践集中在生产者-消费者模型上。以多位任课老师布置作业(生产者)和多位同学分工完成(消费者)为例,队列将任务的两端完全解耦:生产方无需关心由哪个具体消费者执行,消费方也无需过问任务来源。当多个消费者线程并发处理队列任务时,输出结果通常不再保持递增,因为队列仅能约束任务“开始执行”的次序,无法左右每个任务的实际耗时。若机械地均分固定数量的任务,容易导致接手复杂任务的工作者严重超时;而让消费者采取“做完一个再领下一个”的动态抢占机制,则能自然达成负载均衡。 阻塞等待机制防范竞争与资源浪费 (6:05) Python 队列的 `get()` 方法默认具备阻塞特性,在队列为空时会自动挂起线程,待新任务写入后再行唤醒。相比之下,使用 `list` 进行 `while True` 轮询会造成 CPU 资源的无意义空转;若引入 `time.sleep()`,休眠时长设短了依然浪费算力,设长了则会导致任务积压、响应迟钝。更严峻的问题在于缺乏线程安全:在多线程竞争环境下,两个线程若先后通过 `if my_list:` 检查,并在只剩一个元素时同时执行 `pop(0)`,后执行的线程必然因列表为空而触发 `IndexError` 崩溃。若使用列表替代,开发者必须手动额外维护加锁逻辑。 并发退出控制与计数协同机制 (7:44) 多线程消费模型中常出现主程序卡住无法退出的情况,因为任务投递完毕后消费者线程仍在空队列上持续阻塞。若简单将其设为守护线程(`daemon=True`),可能导致主线程瞬间执行完毕并强行杀死尚未处理任务的消费者线程;而仅在主线程监测队列是否为空同样是错误的,因为队列为空仅代表任务已被认领,并不代表计算真正完成。Python 的 `queue.Queue` 提供了内置的协同计数机制:`put()` 递增任务计数,消费者处理完毕后显式调用 `task_done()` 递减计数,主线程则通过 `join()` 实施阻塞,直到所有未完成任务计数归零后才放行退出。 标准先进先出队列是构建并发任务调度的核心基础。高天补充指出,Python 还基于该模型扩展了支持超时控制的非阻塞接口(如 `put_nowait()` 和 `get_nowait()`)、后进先出的 `LifoQueue` 以及基于权重的 `PriorityQueue`,理解基础队列与线程协同是掌握这些衍生工具的前提。

Source quotes

[7:44] 高天: Queue 里面空只代表所有的任务都被认领了,不代表所有的任务都被完成了。

Original transcript

[0:00] 这期视频我们讲Q,也就是对列Q是一个非常简单的线性有序数据结构在大多数情况下,当我们聊到Q的时候我们指的是FIFO,也就是first in, first out最先放进去的数据被最先拿出来当然,我们也偶尔会用到它广义的意思一个可以逐个往里放数据,然后按照一定的顺序输出的数据结构比如说Priority Q一个最接触的Q只需要支持两个功能放和拿我们看左边这个例子我们用python的内置库构建了一个q把0到9按顺序一个一个放进去这里我们用了一个q在实现的时候非常常见的功能判断它是不是空在q没有空的情况下把里面的数据一个一个拿出来然后打印可以看到我们就打印出了0到9这就是我们所谓的fifo数据会按照进入q的顺序被拿出来大家可以把它想象成人的大肠儿有一边近东西有点粗东西那Q这个数据结构有什么用呢最常见的用法是用来有序的安排任务我们可以想象一个在生活中的对列比如说火车站或者迪士尼卖票的地方如果没有对列的存在来一个人他就直接冲到售票窗口去买票那可能会造成很多问题第一个售票窗口可能会拥堵有一堆人超出要买票导致售票本身的效率下降第二个身材弱小的人可能一倍再买不着票因为是谁最强壮谁嗓门最大谁就先买票于是我们就发明了对列大家按照来的先后顺序排成一队然后一个一个的去售票窗口买票如果发现对列太长了我们可以多开几个售票窗口如果对列里没有人了我们可以少开几个售票窗口同时避免了有人可能来得很早但是始终买不到票的情况发生那你有可能会觉得在编程的时候任务来一个解决一个不就完了吗要Q有什么用呢那Q的有序性实际上经常是保证我们算法正确的基础我们举个常见的例子BFS我们定义了个简单的二叉数每个节点的数据只有一个data根节点有一个左孩子一个右孩子左孩子有个左孩子右孩子有个右孩子这个时候我们想做宽度优先搜索我们最最常见的实现方式就是做一个Q这个Q代表应该开始搜哪个节点了对于每一个节点我们都只先搜自己再搜左节点再搜右节点这样就保证了宽度优先的搜索如果我们这里没有用Q来保证这些任务的有序性很有可能就会变成一个深度优先搜索这根节点说我完事了 左孩子你先来结果左孩子说我还有孩子呢 我孩子得来啊这就变成深度搜索了而一个先进先出的Q是实现这个算法很重要的一个环节可以看到我们变类的结果是宽度优先的12345那可能有人会说这里我不一定要用Q对吧对吧我知道Python里面有一个内置的非常方便的数据结构叫做list这里为什么不能直接用list呢逻辑上说当然也可以因为list是支持拿出任意一个数据的我们可以看到用list重写一下可以得到一模一样的结果然而相比q来说list有两个比较严重的问题第一个问题比较直观就是list pop0它并不是OE的对于q来说put和get操作都是常数时间的当然我们这里说的是一般的实现你当然也可以实现出来一个Q它不是常数时间的因为Q本身是一个抽象的数据结构它可以有很多的其他数据结构来实现但是Python和其他绝大多数语言的内置酷的Q协助操作和读出操作都是O1的而ListPop0这个操作是ON的由于List的特殊性它需要把第一个拿走然后把后面所有的东西往前移这是第一点性能上的问题第二点一般和多线程有关它主要涉及到阻塞和线程安全这么一个概念首先我们简单的说一下所谓的producer consumer model什么叫producer consumer model呢其实它是一个非常简单的概念就是这个世界上有任务的产生者还有任务的完成者比如说在学校化学老师布置作业数学老师布置作业物理老师布置作业他们就是任务的产生者producer布置下来这些作业呢你负责数学你同桌负责物理和化学你俩最后把作业一凑完成了老师布置的全部内容你们就是任务的完成者叫consumer那在实际的生活中producer跟consumer的比例可能是各种各样的有可能是100比1也有可能是1比100这个时候呢对列就起到了它的效果因为作为任务的生产方你根本不需要在意我需要把任务交给谁去解决你不用自己去找consumer你只需要把任务放到q里就可以了同样的作为任务的完成者你也不需要去想我应该解决谁的任务你只需要从q里面拿就行了 [5:00] 我们看左边给大家写了一个非常简单的例子作为producer我就把0到9扔到Q里了作为Consumer 我拿到一个树 我就把它打出来我们这里做了一个producer和两个consumer大家可以看到0到9都被打出来了但是并不是按顺序打出来的因为我们只能保证这个任务被开始执行的顺序我们并不能保证这个任务被完成时候的顺序大家可以想象在实际的应用中不同的任务需要的完成时间可能是不一样的那如果我有10个任务我仅仅是把5个分配给consumer把另外五个分配给另外一个consumer这就有可能会导致任务的不均衡一个人拿到的我可能很简单,另外一个人拿到的我可能很复杂而这种利用Q来分配任务完成一个再拿下一个的方式是一种负载均衡 load balancing这里我们注意一下这个Q点get,它是一个我们所谓的阻塞操作什么意思呢如果Q里面没有东西的话他就会停在这直到Q里有东西为止这是一个非常好的特性对吧有任务就做,没有任务就歇着这种阻塞能力是List没有的那你说别急 我知道我可以实现这个操作你说这样不就可以了吗对吧在循环里面如果里面有东西我就拿没有我就接着循环这里有好多个问题第一如果list里没东西你就这么吭哧吭哧循环你实际上是一直在做操作的也就是说你一直在浪费CPU那你说我加一个sleep行不行呢好一些但是你知道应该sleep多久最合适吗Sleep都完了你又要浪费CPUSleep长了 任务都堆积了 你还没来得及做这里就明显没有阻塞灵活当然还有一个更重要的问题就是我们前面提到的线程安全我们注意第七行第八行这两行代码在单线程的时候这两行代码没有任何毛病如果list里有东西就把这个东西拿出来但是在多线程程序里你在进行完第七行的判断之后你有可能会把操作移交给另外一个县城而另外一个线程恰恰也刚好完成了这个ifless的操作于是有可能在list里面只有一个element的情况下你们两个前后脚判断完了这个iflist都开始执行第8行的List & Pop这样就会有一个线程在pop的时候发现lis里面没有东西从而产生异常也就是说这里如果你想要正确工作的话你还需要额外的上一个锁那相比起来是不是这个写法又简单又好刚才可能有小伙伴发现了一件事就是我运行这个程序的时候这个程序没有停看到没有右边的程序实际上还在运行我需要Ctrl+C才让它跳出来这是因为两个consumer线程都被block住了他们两个都还等着从Q里面拿东西呢但是Q里面再也不会生产任何东西了因为producer这个函数已经结束了那在默认情况下,Python进程会等所有的线程结束再退出于是这个成绩就卡在那了那肯定有的小伙伴就会说这个问题我知道怎么解决只要在建立thread的时候,加上这个demon等于true就可以了Demon等于True的意思是只要主线程结束了我就结束如果我们把这个demon等于true加上之后,再运行程序你会发现他什么也没打出来为什么呢?producer飞快的完成了Consumer还什么都没来得及干这里主县城就结束了顺带着把两个子线城就给干掉了所以什么东西都没打出来我们在这里希望产生的效果是什么呢是Q里的任务全部结束之后,我这个进程再结束。肯定有的小朋友来了对吧这我会 我一直监测这个Q是不是空了只要不空 我就等着结果上他看似没有问题但是这里依然涉及了两件事第一前面提到过的到底休息多久第二大家注意一下这个Q里面空了就代表任务完成了吗我们看作为一个consumer他在拿完最后一个任务的时候Q里面就空了但是他拿到这个任务 他可能还没有做完呢Q里面空只代表所有的任务都被认领了不代表所有的任务都被完成了那怎么样来标志所有的任务都完成了呢只有consumer知道什么时候任务完成对吧所以,Python的Q提供了一个特殊的标记方式这就是第8行这里的Task Done在我们每一次往这个Q里面放一个任务的时候Python会在这个Q的内部计输器里加给E而每一次运行task done的时候会把这个技术器 [10:00] eat. So, consumer in every single task task done, only use task done method. You can tell the queue how many tasks are done. In the bottom, we have a Q. Q.Joy will be in this place 直到这个 queued task done和 put the We'll see that the joint is normal. This is our most basic Python producer consumer model. Today we will introduce Q's key. It's a very simple and very useful structure. Python's Q and other functions, such as the Q and the Q, or the get or put time to not be a bit of a bit "遇到问题直接锐行 exception" But in the final queue, Python has also provided some other queue. For example LIFO Cue or PRIORITY Cue They are used to do a campaign Today, we will talk about the most basic information. After this, I believe that you will learn other things will be faster. This is the video I'm going to leave, and don't forget to leave me a comment. Thank you.