Python 中常见的 Queue 有两种:
python
import queue
q = queue.Queue()
python
from multiprocessing import Queue
q = Queue()
它们都提供了 put()、get() 等方法,但解决的问题不同。
1. queue.Queue
queue.Queue 用于同一个进程中的多个线程。
线程共享所属进程的内存,所以它们能够直接操作同一个队列。queue.Queue 内部会加锁,避免多个线程同时操作队列时发生数据混乱。
text
一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> 同一个 queue.Queue
└── 线程 C ─┘
示例:
python
import queue
import threading
q = queue.Queue()
def worker():
q.put("线程写入的数据")
thread = threading.Thread(target=worker)
thread.start()
thread.join()
print(q.get())
简单来说,线程就像同一个房间里的人,queue.Queue 就像房间里的一个公共盒子。
所有人都能直接操作这个盒子,而队列内部的锁可以避免大家同时操作时发生混乱。
2. multiprocessing.Queue
multiprocessing.Queue 用于同一台机器上的多个进程。
不同进程拥有各自独立的内存,因此不能直接操作同一个普通 Python 队列。
text
父进程的内存 子进程的内存
queue.Queue A queue.Queue A 的副本
即使创建子进程时把普通的 queue.Queue 传进去,子进程操作的也只是自己内存中的队列,父进程无法看到它后续写入的数据。
multiprocessing.Queue 通过操作系统管道在进程之间传输数据:
text
进程 A
│
│ 序列化
▼
操作系统管道
│
│ 反序列化
▼
进程 B
示例:
python
from multiprocessing import Process, Queue
def worker(q):
q.put("子进程写入的数据")
if __name__ == "__main__":
q = Queue()
process = Process(target=worker, args=(q,))
process.start()
print(q.get())
process.join()
可以把多个进程想象成住在不同房间里的人。
他们不能直接操作同一个盒子,所以需要一条传送带,把东西从一个房间送到另一个房间。
multiprocessing.Queue 就是这条传送带。
3. 两种 Queue 的核心区别
queue.Queue
解决的是:
多个人在同一个房间里,如何安全地操作同一个盒子。
它依靠共享内存保存数据,并通过线程锁保证操作安全。
text
一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> queue.Queue
└── 线程 C ─┘
multiprocessing.Queue
解决的是:
人在不同房间里,如何把东西从一个房间送到另一个房间。
它通过序列化、操作系统管道等方式在不同进程之间传递数据。
text
进程 A ──> 操作系统管道 ──> 进程 B
4. 为什么分布式 Manager 使用 queue.Queue?
在使用 BaseManager 实现分布式通信时,真正的队列和数据都保存在 Manager 服务端进程里。
其他机器并不会直接操作这个队列,而是通过网络告诉 Manager:
请帮我执行
put()。
或者:
请帮我执行
get()。
例如客户端执行:
python
q.put("任务 A")
看起来像是在直接操作队列,实际上执行过程是:
text
客户端
│
│ 网络请求:请执行 put("任务 A")
▼
Manager 服务端
│
▼
服务端执行 queue.Queue.put("任务 A")
│
▼
数据保存在服务端进程的内存中
客户端拿到的 q 不是真正的 queue.Queue,而是一个代理对象。
这个代理对象负责把客户端的方法调用转换成网络请求。
text
客户端代理对象
│
│ 网络通信
▼
Manager 服务端
│
▼
真正的 queue.Queue
因此,在这种分布式架构中:
- Manager 负责跨进程、跨机器通信;
queue.Queue负责在服务端进程中保存数据;- 客户端只负责发送操作命令;
- 真正的
put()和get()都在服务端执行; - 数据始终保存在服务端进程中。
5. 多个客户端访问时会发生什么?
多个客户端可以同时向 Manager 发送请求:
text
客户端 A ─┐
客户端 B ─┼── 网络请求 ──> Manager 服务端 ──> queue.Queue
客户端 C ─┘
Manager 服务端可能使用多个线程处理这些请求:
text
客户端 A ──> Manager 服务线程 A ─┐
客户端 B ──> Manager 服务线程 B ─┼──> 同一个 queue.Queue
客户端 C ──> Manager 服务线程 C ─┘
这些服务线程都位于同一个 Manager 服务端进程中,因此它们共享服务端进程的内存,也能访问同一个 queue.Queue。
这正是 queue.Queue 擅长处理的情况:
text
同一个进程中的多个线程
│
▼
同一个 queue.Queue
6. Manager 会自动给队列加锁吗?
Manager 会保护自己的内部状态,例如:
- 网络连接;
- 对象注册信息;
- 代理对象;
- 引用计数。
但是,Manager 不会自动给注册对象的每个方法加一把全局锁。
也就是说,Manager 不会自动执行下面这样的逻辑:
python
with global_lock:
task_queue.put("任务")
多个客户端请求可能在不同的 Manager 服务线程中同时执行,因此真正保存数据的对象仍然需要保证线程安全。
queue.Queue 自己已经实现了线程锁,所以多个 Manager 服务线程同时调用它的 put() 或 get() 时,不会把内部数据弄乱。
它们的职责可以这样理解:
text
Manager
│
│ 负责把客户端命令送到服务端
▼
queue.Queue
│
│ 负责安全地执行队列操作
▼
服务端内存中的数据
因此,不是 Manager 自动把普通对象变成了线程安全对象,而是 queue.Queue 本身就是线程安全的。
7. 为什么不使用 multiprocessing.Queue?
不是完全不能使用,而是在典型的 Manager 分布式架构中通常没有必要。
Manager 已经解决了跨进程、跨机器通信的问题。
如果服务端再使用 multiprocessing.Queue,就相当于又增加了一层进程间通信机制。
使用 multiprocessing.Queue 时,数据路径类似:
text
客户端
│
│ Manager 网络通信
▼
Manager 服务端
│
│ 再进行序列化和管道传输
▼
multiprocessing.Queue
而使用 queue.Queue 时,数据路径更加直接:
text
客户端
│
│ Manager 网络通信
▼
Manager 服务端
│
│ 直接操作服务端内存
▼
queue.Queue
multiprocessing.Queue 内部通常还需要:
- 对数据进行序列化;
- 使用操作系统管道;
- 使用进程锁和信号量;
- 使用后台线程传输数据。
这些机制的目的是让不同进程能够直接传递数据。
但在 Manager 分布式架构中,客户端并不直接操作服务端队列。Manager 已经把客户端命令传到服务端了,所以没有必要再使用一层进程间管道。
因此:
queue.Queue不是负责分布式通信的,Manager 才负责分布式通信。
queue.Queue只是 Manager 服务端进程内部保存数据的线程安全容器。
8. 三种场景总结
多线程
text
一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> queue.Queue
└── 线程 C ─┘
使用:
python
import queue
q = queue.Queue()
本机多进程
text
进程 A ──> 操作系统管道 ──> 进程 B
使用:
python
from multiprocessing import Queue
q = Queue()
多机器分布式通信
text
客户端 A ─┐
客户端 B ─┼──> Manager 服务端 ──> queue.Queue
客户端 C ─┘
使用:
python
import queue
from multiprocessing.managers import BaseManager
task_queue = queue.Queue()
9. 最终结论
选择哪一种 Queue,取决于谁在直接操作队列。
text
同一个进程中的多个线程
└── queue.Queue
本机多个进程直接传递数据
└── multiprocessing.Queue
多个机器通过 Manager 访问服务端数据
└── Manager + queue.Queue
最重要的理解是:
queue.Queue让同一个进程中的多个线程安全地操作共享数据。
multiprocessing.Queue通过管道把数据从一个进程传递到另一个进程。
在分布式 Manager 场景中,客户端并不直接操作队列,只是通过网络向服务端发送命令;真正的队列和数据仍然位于 Manager 服务端进程中。