Python 多线程之间如何安全通信?queue.Queue 为什么常用于生产者消费者模型?
简化版
多线程通信不要优先共享列表再手动加锁,推荐使用 queue.Queue。它是线程安全队列,支持阻塞 put() / get()、超时、任务完成通知,特别适合生产者消费者模型。
详细版
生产者消费者示例:
import queue
import threading
q = queue.Queue(maxsize=100)
def producer():
for item in range(1000):
q.put(item)
q.put(None)
def consumer():
while True:
item = q.get()
try:
if item is None:
break
handle(item)
finally:
q.task_done()
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
Queue 的价值:
- 内部已经处理锁和条件变量;
- 队列为空时
get()可以阻塞等待; - 队列满时
put()可以阻塞,形成背压; task_done()和join()可以等待任务处理完;maxsize可以防止生产速度远超消费速度导致内存暴涨。
面试回答重点:Queue 能把共享内存修改变成消息传递,减少锁的复杂度。
完整版教学
一、为什么共享列表不够好
如果多个线程都操作一个普通列表:
items.append(x)
item = items.pop(0)
你需要自己处理:
- 列表为空时消费者怎么办;
- 多个消费者同时 pop 怎么办;
- 生产速度过快内存涨怎么办;
- 如何通知消费者停止;
- 如何等待所有任务完成。
这些细节叠在一起,很容易写出竞态条件或死锁。queue.Queue 就是为线程间安全传递数据准备的。
二、Queue 的阻塞语义
消费者调用:
item = q.get()
如果队列为空,默认会阻塞等待。生产者调用:
q.put(item)
如果队列有 maxsize 且已满,默认会阻塞等待。
这种阻塞不是坏事,它形成了自然的协调机制:没有任务就等,有太多任务就让生产者慢下来。
三、maxsize 是背压机制
没有大小限制时,生产者如果非常快,队列可能越堆越大,占满内存。
q = queue.Queue(maxsize=1000)
设置 maxsize 后,队列满了生产者会等待,消费端处理一些后再继续生产。这就是背压:下游处理不过来时,上游不要无限制生产。
四、task_done 和 join 怎么配合
q.join() 会等待所有已入队任务都被标记完成。消费者处理完一个任务后调用 task_done():
def consumer():
while True:
item = q.get()
try:
handle(item)
finally:
q.task_done()
for item in items:
q.put(item)
q.join()
注意:每次 get() 对应一次 task_done()。少调用会导致 join() 永远等待,多调用会抛异常。
五、如何优雅停止消费者
常见方式是发送哨兵对象:
STOP = object()
for _ in range(worker_count):
q.put(STOP)
每个消费者拿到 STOP 后退出:
if item is STOP:
break
为什么要放多个 STOP?因为有多个消费者时,一个 STOP 通常只能让一个消费者退出。
六、Queue 不是万能的
Queue 适合线程间任务分发,但不适合所有并发问题:
- 需要 CPU 密集并行时,应考虑多进程;
- 需要高并发网络 I/O 时,asyncio 可能更轻;
- 需要跨机器通信时,要用消息队列、数据库或网络协议;
- 需要优先级时,可以看
PriorityQueue。
但在多线程生产者消费者模型里,Queue 通常是最稳的起点。
七、常见误区与追问
| 能力 | queue.Queue 的作用 | 面试关注点 |
|---|---|---|
| 线程安全 | 内部用锁和条件变量保护 | 不必手动给队列操作加锁 |
| 阻塞等待 | put/get 可阻塞 | 避免忙等浪费 CPU |
| 背压 | maxsize 限制积压 | 防止生产过快撑爆内存 |
| 完成确认 | task_done/join | 统计任务完成而非只统计取走 |
- 误区:list 加一把锁就等价于 Queue。 列表加锁只能保护结构,阻塞等待、背压、任务完成确认都要额外实现,容易漏边界。
- 误区:qsize() 可以作为可靠并发判断。 多线程下队列大小只是瞬时值,检查后马上可能变化,不应依赖它决定是否安全
get。 - 误区:消费者线程用死循环就能优雅退出。 没有停止信号时线程可能永久阻塞在
get(),常见做法是发送哨兵对象或配合事件。 - 追问:task_done() 调多了会怎样? 会破坏未完成任务计数,可能抛出异常;每个成功
get()的任务应该只对应一次task_done()。 - 追问:join() 等待的是什么? 等待队列中所有已入队任务被消费者处理并调用
task_done(),不是等待线程对象结束。 - 追问:maxsize 设置太小或太大有什么影响? 太小会让生产者频繁阻塞,太大可能掩盖消费慢的问题并增加内存积压。
记忆钩子:
Queue不是只会“装数据”,它同时提供线程安全、阻塞等待、背压和任务完成确认。
八、加强记忆
线程间通信优先想“传消息”,不要上来就共享变量加锁。queue.Queue 把锁、等待、通知、背压都封装好了:生产者 put,消费者 get,处理完 task_done,主线程用 join 等待收尾。它是 Python 多线程里最实用的安全通信工具之一。