在Python中选择子进程、多进程和线程的标准?

129 投票
6 回答
58298 浏览
提问于 2025-04-15 21:32

我想让我的Python程序能够同时使用机器上的多个处理器。我的并行处理非常简单,所有的“线程”都是独立的,并且它们的输出会写入不同的文件。我不需要这些线程之间交换信息,但我必须知道它们什么时候完成,因为我的某些步骤依赖于它们的输出。

可移植性很重要,我希望这个程序能在Mac、Linux和Windows的任何Python版本上运行。考虑到这些要求,哪个Python模块最适合实现这个功能呢?我在考虑thread、subprocess和multiprocessing这几个选项,它们似乎都能提供相关的功能。

对此有什么想法吗?我希望找到一个最简单且可移植的解决方案。

6 个回答

5

在类似的情况下,我选择了使用独立的进程,并通过网络套接字进行必要的通信。这种方式非常便携,而且用Python实现起来也相对简单,但可能不是最简单的选择(在我的情况下,我还有一个额外的限制:需要与用C++编写的其他进程进行通信)。

在你的情况下,我可能会选择多进程,因为在使用CPython时,Python的线程并不是真正的线程。嗯,它们是本地系统线程,但从Python调用的C模块可能会释放或不释放全局解释器锁(GIL),这会影响其他线程在调用阻塞代码时的运行。

218

对我来说,这其实很简单:

subprocess 选项:

subprocess 是用来运行其他可执行程序的——它基本上是对 os.fork()os.execve() 的一种封装,并且支持一些可选的设置(比如设置从子进程到主进程的管道)。当然,你也可以使用其他进程间通信(IPC)机制,比如套接字,或者 Posix 或 SysV 共享内存。但你会受到你调用的程序所支持的接口和 IPC 通道的限制。

通常,人们会同步使用 subprocess——简单地调用一些外部工具,并读取它的输出或等待它完成(也许是从临时文件中读取结果,或者在它将结果发送到某个数据库后读取)。

不过,你也可以创建数百个子进程并对它们进行轮询。我个人最喜欢的工具 classh 就是这样做的。subprocess 模块的最大缺点 是输入输出支持通常是阻塞的。未来某个版本的 Python 3.x 有一个草案 PEP-3145 来解决这个问题,还有一个替代方案 asyncproc(警告:这个链接直接指向下载,而不是任何文档或 README)。我发现直接导入 fcntl 并直接操作你的 Popen 管道文件描述符也相对简单——不过我不知道这是否适用于非 UNIX 平台。

(更新:2019年8月7日:Python 3 对异步子进程的支持:asyncio 子进程

subprocess 几乎没有事件处理支持……不过你可以使用 signal 模块和传统的 UNIX/Linux 信号——可以温和地终止你的进程。

multiprocessing 选项:

multiprocessing 是用来在你现有的(Python)代码中运行函数,并支持更灵活的进程间通信。在可能的情况下,最好围绕模块的 Queue 对象构建你的 multiprocessing IPC,但你也可以使用 Event 对象和其他各种特性(其中一些可能是基于 mmap 支持的)。

Python 的 multiprocessing 模块旨在提供与 threading 非常相似的接口和功能,同时允许 CPython 在多个 CPU/核心之间扩展你的处理,尽管存在全局解释器锁(GIL)。它利用了操作系统内核开发者所做的细粒度 SMP 锁定和一致性工作。

threading 选项:

threading 是为相对狭窄的应用范围设计的,这些应用是 I/O 密集型的(不需要跨多个 CPU 核心扩展),并且受益于线程切换的极低延迟和切换开销(与进程/上下文切换相比)。在 Linux 上,这几乎是一个空集合(Linux 的进程切换时间与线程切换时间非常接近)。

threading 在 Python 中有两个主要缺点

一个当然是实现特定的——主要影响 CPython。那就是 GIL。大多数情况下,大多数 CPython 程序不会从超过两个 CPU(核心)的可用性中受益,通常性能会因为 GIL 锁定竞争而受到影响

更大的问题是,线程共享相同的内存、信号处理程序、文件描述符和某些其他操作系统资源。因此,程序员必须非常小心对象锁定、异常处理和代码的其他方面,这些方面都很微妙,可能会导致整个进程(线程组)崩溃、停滞或死锁。

相比之下,multiprocessing 模型为每个进程提供自己的内存、文件描述符等。在其中任何一个进程中发生崩溃或未处理的异常只会影响该资源,稳健地处理子进程或兄弟进程的消失通常比调试、隔离和修复线程中的类似问题要容易得多。

  • (注意:在主要 Python 系统中使用 threading,例如 NumPy,可能会比大多数你自己的 Python 代码受到的 GIL 竞争影响小得多。这是因为它们经过特别设计;例如,NumPy 的本地/二进制部分在安全时会释放 GIL。)

twisted 选项:

还值得注意的是,Twisted 提供了另一种选择,这种选择既优雅又非常难以理解。基本上,冒着过于简化的风险,Twisted 提供了在任何(单个)进程中基于事件驱动的协作多任务处理。

要理解这是如何实现的,你应该了解 select() 的特性(它可以围绕 select()poll() 或类似的操作系统系统调用构建)。基本上,这一切都是通过向操作系统请求在一组文件描述符上等待任何活动或某个超时来驱动的。

从每个这些 select() 调用中唤醒都是一个事件——要么是涉及某些套接字或文件描述符上可用(可读)输入的事件,要么是某些其他(可写)描述符或套接字上可用的缓冲空间,或者某些异常条件(例如,TCP 的带外 PUSH 数据包),或者是超时。

因此,Twisted 编程模型围绕处理这些事件构建,然后循环在结果“主”处理程序上,允许它将事件分发到你的处理程序。

我个人认为,Twisted 这个名字很能体现编程模型……因为你解决问题的方法在某种意义上必须是“扭曲”的。与其将你的程序视为对输入数据和输出或结果的一系列操作,不如说你是在编写一个服务或守护进程,并定义它如何对各种事件作出反应。(事实上,Twisted 程序的核心“主循环”通常是 reactor())。

使用 Twisted 的主要挑战在于要适应事件驱动模型,并且避免使用任何未能与 Twisted 框架协作的类库或工具包。这就是为什么 Twisted 提供了自己的 SSH 协议处理模块、curses,以及自己的子进程/Popen 函数,还有许多其他模块和协议处理程序,乍一看似乎与 Python 标准库中的内容重复。

我认为,即使你从未打算使用 Twisted,理解它的概念层面也是有用的。它可能会为你在处理线程、进程和子进程时的性能、竞争和事件处理提供一些见解,以及你进行的任何分布式处理。

(注意:更新版本的 Python 3.x 正在包含 asyncio(异步 I/O)特性,如 async def@async.coroutine 装饰器和 await 关键字,以及 yield from future 支持。所有这些在进程(协作多任务)方面大致类似于Twisted。)(关于 Twisted 对 Python 3 的当前支持状态,请查看:https://twistedmatrix.com/documents/current/core/howto/python3.html)

distributed 选项:

还有一个你没有提到的处理领域,但值得考虑的是分布式处理。对于分布式处理和并行计算,有许多 Python 工具和框架。就我个人而言,我认为最容易使用的工具是那些最少被认为属于这个领域的工具。

围绕 Redis 构建分布式处理几乎是微不足道的。整个键值存储可以用来存储工作单元和结果,Redis 列表可以用作类似 Queue() 的对象,而 PUB/SUB 支持可以用于类似 Event 的处理。你可以对键进行哈希,并使用跨松散集群的 Redis 实例复制的值来存储拓扑和哈希令牌映射,以提供一致的哈希和故障转移,从而超越任何单个实例的容量来协调你的工作者并在它们之间传递数据(可以是 pickle、JSON、BSON 或 YAML 格式)。

当然,当你开始围绕 Redis 构建更大规模和更复杂的解决方案时,你实际上是在重新实现许多已经通过 CeleryApache SparkHadoopZookeeperetcdCassandra 等解决的功能。这些都有 Python 访问其服务的模块。

[更新:如果你考虑在分布式系统中使用 Python 进行计算密集型处理,这里有几个资源供你参考:IPython ParallelPySpark。虽然这些是通用的分布式计算系统,但它们在数据科学和分析领域特别易于访问和流行。]

结论

以上就是 Python 的各种处理选择,从单线程、简单的同步调用子进程,到轮询子进程池、线程和多进程、事件驱动的协作多任务处理,以及分布式处理。

67

multiprocessing 是一个非常实用的模块,就像瑞士军刀一样多功能。它比线程更通用,因为你甚至可以进行远程计算。所以我建议你使用这个模块。

subprocess 模块也可以让你启动多个进程,但我觉得它用起来没有新的 multiprocessing 模块方便。

线程的使用比较复杂,尤其是在 CPython 中,通常只能使用一个核心(尽管有评论提到,从 Python 代码调用的 C 代码可以释放全局解释器锁(GIL))。

我认为你提到的这三个模块的大部分功能都是可以跨平台使用的。关于可移植性,值得注意的是 multiprocessing 从 Python 2.6 开始才成为标准模块(不过确实存在一些旧版本的 Python 的版本)。但这个模块真的很棒!

撰写回答