← 返回题目列表

Python 多线程之间如何安全通信?queue.Queue 为什么常用于生产者消费者模型?

高频 中等 第 3 / 27 题 更新于 2026/07/27
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 多线程里最实用的安全通信工具之一。