Python 中两种 Queue 的区别

6 阅读6分钟

Python 中常见的 Queue 有两种:

import queue

q = queue.Queue()
from multiprocessing import Queue

q = Queue()

它们都提供了 put()get() 等方法,但解决的问题不同。

1. queue.Queue

queue.Queue 用于同一个进程中的多个线程。

线程共享所属进程的内存,所以它们能够直接操作同一个队列。queue.Queue 内部会加锁,避免多个线程同时操作队列时发生数据混乱。

一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> 同一个 queue.Queue
└── 线程 C ─┘

示例:

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 队列。

父进程的内存                  子进程的内存

queue.Queue A                 queue.Queue A 的副本

即使创建子进程时把普通的 queue.Queue 传进去,子进程操作的也只是自己内存中的队列,父进程无法看到它后续写入的数据。

multiprocessing.Queue 通过操作系统管道在进程之间传输数据:

进程 A
   │
   │ 序列化
   ▼
操作系统管道
   │
   │ 反序列化
   ▼
进程 B

示例:

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

解决的是:

多个人在同一个房间里,如何安全地操作同一个盒子。

它依靠共享内存保存数据,并通过线程锁保证操作安全。

一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> queue.Queue
└── 线程 C ─┘

multiprocessing.Queue

解决的是:

人在不同房间里,如何把东西从一个房间送到另一个房间。

它通过序列化、操作系统管道等方式在不同进程之间传递数据。

进程 A ──> 操作系统管道 ──> 进程 B

4. 为什么分布式 Manager 使用 queue.Queue

在使用 BaseManager 实现分布式通信时,真正的队列和数据都保存在 Manager 服务端进程里。

其他机器并不会直接操作这个队列,而是通过网络告诉 Manager:

请帮我执行 put()

或者:

请帮我执行 get()

例如客户端执行:

q.put("任务 A")

看起来像是在直接操作队列,实际上执行过程是:

客户端
   │
   │ 网络请求:请执行 put("任务 A")
   ▼
Manager 服务端
   │
   ▼
服务端执行 queue.Queue.put("任务 A")
   │
   ▼
数据保存在服务端进程的内存中

客户端拿到的 q 不是真正的 queue.Queue,而是一个代理对象。

这个代理对象负责把客户端的方法调用转换成网络请求。

客户端代理对象
      │
      │ 网络通信
      ▼
Manager 服务端
      │
      ▼
真正的 queue.Queue

因此,在这种分布式架构中:

  • Manager 负责跨进程、跨机器通信;
  • queue.Queue 负责在服务端进程中保存数据;
  • 客户端只负责发送操作命令;
  • 真正的 put()get() 都在服务端执行;
  • 数据始终保存在服务端进程中。

5. 多个客户端访问时会发生什么?

多个客户端可以同时向 Manager 发送请求:

客户端 A ─┐
客户端 B ─┼── 网络请求 ──> Manager 服务端 ──> queue.Queue
客户端 C ─┘

Manager 服务端可能使用多个线程处理这些请求:

客户端 A ──> Manager 服务线程 A ─┐
客户端 B ──> Manager 服务线程 B ─┼──> 同一个 queue.Queue
客户端 C ──> Manager 服务线程 C ─┘

这些服务线程都位于同一个 Manager 服务端进程中,因此它们共享服务端进程的内存,也能访问同一个 queue.Queue

这正是 queue.Queue 擅长处理的情况:

同一个进程中的多个线程
            │
            ▼
     同一个 queue.Queue

6. Manager 会自动给队列加锁吗?

Manager 会保护自己的内部状态,例如:

  • 网络连接;
  • 对象注册信息;
  • 代理对象;
  • 引用计数。

但是,Manager 不会自动给注册对象的每个方法加一把全局锁。

也就是说,Manager 不会自动执行下面这样的逻辑:

with global_lock:
    task_queue.put("任务")

多个客户端请求可能在不同的 Manager 服务线程中同时执行,因此真正保存数据的对象仍然需要保证线程安全。

queue.Queue 自己已经实现了线程锁,所以多个 Manager 服务线程同时调用它的 put()get() 时,不会把内部数据弄乱。

它们的职责可以这样理解:

Manager
   │
   │ 负责把客户端命令送到服务端
   ▼
queue.Queue
   │
   │ 负责安全地执行队列操作
   ▼
服务端内存中的数据

因此,不是 Manager 自动把普通对象变成了线程安全对象,而是 queue.Queue 本身就是线程安全的。

7. 为什么不使用 multiprocessing.Queue

不是完全不能使用,而是在典型的 Manager 分布式架构中通常没有必要。

Manager 已经解决了跨进程、跨机器通信的问题。

如果服务端再使用 multiprocessing.Queue,就相当于又增加了一层进程间通信机制。

使用 multiprocessing.Queue 时,数据路径类似:

客户端
   │
   │ Manager 网络通信
   ▼
Manager 服务端
   │
   │ 再进行序列化和管道传输
   ▼
multiprocessing.Queue

而使用 queue.Queue 时,数据路径更加直接:

客户端
   │
   │ Manager 网络通信
   ▼
Manager 服务端
   │
   │ 直接操作服务端内存
   ▼
queue.Queue

multiprocessing.Queue 内部通常还需要:

  • 对数据进行序列化;
  • 使用操作系统管道;
  • 使用进程锁和信号量;
  • 使用后台线程传输数据。

这些机制的目的是让不同进程能够直接传递数据。

但在 Manager 分布式架构中,客户端并不直接操作服务端队列。Manager 已经把客户端命令传到服务端了,所以没有必要再使用一层进程间管道。

因此:

queue.Queue 不是负责分布式通信的,Manager 才负责分布式通信。

queue.Queue 只是 Manager 服务端进程内部保存数据的线程安全容器。

8. 三种场景总结

多线程

一个进程
├── 线程 A ─┐
├── 线程 B ─┼──> queue.Queue
└── 线程 C ─┘

使用:

import queue

q = queue.Queue()

本机多进程

进程 A ──> 操作系统管道 ──> 进程 B

使用:

from multiprocessing import Queue

q = Queue()

多机器分布式通信

客户端 A ─┐
客户端 B ─┼──> Manager 服务端 ──> queue.Queue
客户端 C ─┘

使用:

import queue
from multiprocessing.managers import BaseManager

task_queue = queue.Queue()

9. 最终结论

选择哪一种 Queue,取决于谁在直接操作队列。

同一个进程中的多个线程
    └── queue.Queue

本机多个进程直接传递数据
    └── multiprocessing.Queue

多个机器通过 Manager 访问服务端数据
    └── Manager + queue.Queue

最重要的理解是:

queue.Queue 让同一个进程中的多个线程安全地操作共享数据。

multiprocessing.Queue 通过管道把数据从一个进程传递到另一个进程。

在分布式 Manager 场景中,客户端并不直接操作队列,只是通过网络向服务端发送命令;真正的队列和数据仍然位于 Manager 服务端进程中。