PEP 371 – 将 multiprocessing 包添加到标准库
- 作者:
- Jesse Noller <jnoller at gmail.com>,Richard Oudkerk <r.m.oudkerk at googlemail.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2008年5月6日
- Python 版本:
- 2.6, 3.0
- 发布历史:
- 2008年6月3日
摘要
本 PEP 提出将 pyProcessing
[1] 包包含到 Python 标准库中,并将其重命名为“multiprocessing”。
processing
包模仿标准库 threading
模块的功能,提供了一种基于进程的线程编程方法,允许最终用户调度多个任务,从而有效地绕过全局解释器锁。
该包还提供服务器和客户端功能( processing.Manager
),以提供对对象和任务的远程共享和管理,以便应用程序不仅可以利用本地计算机上的多个内核,还可以跨网络计算机集群分配对象和任务。
虽然该包的分布式功能很有益,但本 PEP 的主要重点是该包的核心类似线程的 API 和功能。
基本原理
当前的 CPython 解释器实现了全局解释器锁 (GIL),并且在 Python 3000 或当前计划的其他版本 [2] 中进行工作之前,GIL 在可预见的未来将保留在 CPython 解释器中。虽然 GIL 本身使解释器和扩展基础的 C 代码保持简洁易于维护,但对于那些利用多核机器的 Python 程序员来说,它经常是一个问题。
GIL 本身阻止了多个线程在任何给定时间点在解释器中运行,有效地消除了 Python 利用多处理器系统的能力。
pyprocessing 包提供了一种绕过 GIL 的方法,允许 CPython 中的应用程序利用多核架构,而无需用户完全改变其编程范式(即:放弃线程编程而采用另一种“并发”方法 - Twisted、Actors 等)。
Processing 包为 CPython 提供了一个“已知 API”,它以 PEP 8 兼容的方式镜像 threading API,具有已知的语义和易于扩展性。
将来,如果 CPython 解释器启用“真正的”线程,该包可能不那么相关,但是对于某些应用程序来说,分叉操作系统进程有时可能比使用轻量级线程更可取,尤其是在那些进程创建速度快且经过优化的平台上。
例如,一个简单的线程应用程序
from threading import Thread as worker
def afunc(number):
print number * 3
t = worker(target=afunc, args=(4,))
t.start()
t.join()
pyprocessing 包很好地镜像了 API,只需简单地将导入更改为
from processing import process as worker
代码现在将通过 processing.process 类执行。显然,随着 API 重命名为符合 PEP 8,用户应用程序中还需要进行其他重命名,但这些都是很小的改动。
这种类型的兼容性意味着,通过对代码进行少量(在大多数情况下)的更改,用户应用程序将能够利用给定机器上的所有内核和处理器进行并行执行。在许多情况下,pyprocessing 包甚至比 I/O 绑定程序的普通线程方法更快。当然,这需要考虑到 pyprocessing 包是用优化的 C 代码编写的,而 threading 模块则不是。
“分布式”问题
在关于包含此包的 Python-Dev 讨论中 [3],人们对本 PEP 的意图感到困惑,试图解决“分布式”问题 - 经常将此包的功能与其他解决方案(如基于 MPI 的通信 [4]、CORBA 或其他分布式对象方法 [5])进行比较。
“分布式”问题很大且种类繁多。每个在此领域工作的程序员要么对他们最喜欢的模块/方法有非常强烈的意见,要么有一个高度定制的问题,而没有现有的解决方案可以解决。
接受此包并不排除或建议在“分布式”问题上工作的程序员不检查其问题域的其他解决方案。包含此包的目的是提供本地并发性的入门级功能以及跨网络机器扩展该并发的基本支持 - 尽管两者没有紧密耦合,但实际上 pyprocessing 包可以与任何其他解决方案(包括 MPI 等)结合使用。
如有必要 - 可以将包的本地并发能力与包的网络可访问/共享方面完全解耦。但是,如果没有严重的问题或原因,本 PEP 的作者不建议采用这种方法。
性能比较
众所周知 - 有“谎言、该死的谎言和基准”。这些速度比较虽然旨在展示 pyprocessing 包的性能,但绝不是全面或适用于所有可能的用例或环境。尤其是对于那些进程分叉时间缓慢的平台。
所有基准测试均使用以下内容运行
- 4 核英特尔至强 CPU @ 3.00GHz
- 16 GB RAM
- 在 Gentoo Linux(内核 2.6.18.6)上编译的 Python 2.5.2
- pyProcessing 0.52
所有代码都可以从 http://jessenoller.com/code/bench-src.tgz 下载
这些基准测试的基本执行方法在 run_benchmarks.py [6] 脚本中,它只是一个包装器,用于通过单线程(线性)、多线程(通过 threading)和多进程(通过 pyprocessing)函数为静态数量的迭代执行目标函数,并增加执行循环和/或线程的数量。
run_benchmarks.py 脚本执行每个函数 100 次,通过 timeit 模块选择 100 次迭代中最佳的运行。
首先,为了确定工作进程生成的开销,我们执行一个函数,该函数只是一个 pass 语句(空)
cmd: python run_benchmarks.py empty_func.py
Importing empty_func
Starting tests ...
non_threaded (1 iters) 0.000001 seconds
threaded (1 threads) 0.000796 seconds
processes (1 procs) 0.000714 seconds
non_threaded (2 iters) 0.000002 seconds
threaded (2 threads) 0.001963 seconds
processes (2 procs) 0.001466 seconds
non_threaded (4 iters) 0.000002 seconds
threaded (4 threads) 0.003986 seconds
processes (4 procs) 0.002701 seconds
non_threaded (8 iters) 0.000003 seconds
threaded (8 threads) 0.007990 seconds
processes (8 procs) 0.005512 seconds
如您所见,通过 pyprocessing 包进行进程分叉的速度快于构建和随后执行代码的线程版本的速度。
第二个测试在每个线程(隔离且不共享任何内容)中计算 50000 个斐波那契数。
cmd: python run_benchmarks.py fibonacci.py
Importing fibonacci
Starting tests ...
non_threaded (1 iters) 0.195548 seconds
threaded (1 threads) 0.197909 seconds
processes (1 procs) 0.201175 seconds
non_threaded (2 iters) 0.397540 seconds
threaded (2 threads) 0.397637 seconds
processes (2 procs) 0.204265 seconds
non_threaded (4 iters) 0.795333 seconds
threaded (4 threads) 0.797262 seconds
processes (4 procs) 0.206990 seconds
non_threaded (8 iters) 1.591680 seconds
threaded (8 threads) 1.596824 seconds
processes (8 procs) 0.417899 seconds
第三个测试计算 100000 以下所有素数的总和,同样不共享任何内容。
cmd: run_benchmarks.py crunch_primes.py
Importing crunch_primes
Starting tests ...
non_threaded (1 iters) 0.495157 seconds
threaded (1 threads) 0.522320 seconds
processes (1 procs) 0.523757 seconds
non_threaded (2 iters) 1.052048 seconds
threaded (2 threads) 1.154726 seconds
processes (2 procs) 0.524603 seconds
non_threaded (4 iters) 2.104733 seconds
threaded (4 threads) 2.455215 seconds
processes (4 procs) 0.530688 seconds
non_threaded (8 iters) 4.217455 seconds
threaded (8 threads) 5.109192 seconds
processes (8 procs) 1.077939 seconds
测试二和三专注于纯数值计算的原因是展示当前线程实现如何阻碍非 I/O 应用程序。显然,这些测试可以改进为使用队列来协调结果和工作块,但这对于显示包和核心 processing.process 模块的性能不是必需的。
下一个测试是一个 I/O 绑定测试。这通常是我们看到线程模块方法相对于单线程方法有大幅改进的地方。在这种情况下,每个工作程序都将打开对 lorem.txt 的描述符,在其中随机查找并将行写入 /dev/null
cmd: python run_benchmarks.py file_io.py
Importing file_io
Starting tests ...
non_threaded (1 iters) 0.057750 seconds
threaded (1 threads) 0.089992 seconds
processes (1 procs) 0.090817 seconds
non_threaded (2 iters) 0.180256 seconds
threaded (2 threads) 0.329961 seconds
processes (2 procs) 0.096683 seconds
non_threaded (4 iters) 0.370841 seconds
threaded (4 threads) 1.103678 seconds
processes (4 procs) 0.101535 seconds
non_threaded (8 iters) 0.749571 seconds
threaded (8 threads) 2.437204 seconds
processes (8 procs) 0.203438 seconds
如您所见,pyprocessing 在此 I/O 操作中仍然比使用多个线程快。使用多个线程本身比单线程执行慢。
最后,我们将运行一个基于套接字的测试来显示网络 I/O 性能。此函数从 LAN 上的服务器获取一个 URL,该服务器是 tomcat 中的一个简单的错误页面。它获取该页面 100 次。网络处于静默状态,并且是 10G 连接。
cmd: python run_benchmarks.py url_get.py
Importing url_get
Starting tests ...
non_threaded (1 iters) 0.124774 seconds
threaded (1 threads) 0.120478 seconds
processes (1 procs) 0.121404 seconds
non_threaded (2 iters) 0.239574 seconds
threaded (2 threads) 0.146138 seconds
processes (2 procs) 0.138366 seconds
non_threaded (4 iters) 0.479159 seconds
threaded (4 threads) 0.200985 seconds
processes (4 procs) 0.188847 seconds
non_threaded (8 iters) 0.960621 seconds
threaded (8 threads) 0.659298 seconds
processes (8 procs) 0.298625 seconds
我们最终看到线程性能超过了单线程执行,但当增加工作程序数量时,pyprocessing 包仍然更快。如果您坚持使用一两个线程/工作程序,那么线程和 pyprocessing 之间的时间相当接近。
但是,需要注意的一点是,pyprocessing 包的 Queue
实现中存在隐式开销,这是由于对象序列化造成的。
Alec Thomas 提供了一个基于 run_benchmarks.py 脚本的简短示例,以演示此开销与默认 Queue
实现的对比。
cmd: run_bench_queue.py
non_threaded (1 iters) 0.010546 seconds
threaded (1 threads) 0.015164 seconds
processes (1 procs) 0.066167 seconds
non_threaded (2 iters) 0.020768 seconds
threaded (2 threads) 0.041635 seconds
processes (2 procs) 0.084270 seconds
non_threaded (4 iters) 0.041718 seconds
threaded (4 threads) 0.086394 seconds
processes (4 procs) 0.144176 seconds
non_threaded (8 iters) 0.083488 seconds
threaded (8 threads) 0.184254 seconds
processes (8 procs) 0.302999 seconds
其他基准测试可以在 pyprocessing 包的源代码分发包的 examples/ 目录中找到。这些示例将包含在包的文档中。
维护
pyprocessing 包的作者 Richard M. Oudkerk 已同意在 Python SVN 中维护该包。Jesse Noller 自愿帮助维护/记录和测试该包。
API 命名
虽然该包的 API 的目标旨在与 python 2.x 版本的 threading 和 Queue
模块紧密匹配,但这些模块不符合 PEP 8。已决定不“按原样”添加该包,从而延续不符合 PEP 8 的命名,而是将所有 API、类等重命名为完全符合 PEP 8。
此更改确实影响了那些使用 threading 模块的用户轻松替换的便捷性,但在作者看来,这是一个可以接受的副作用,尤其是在考虑到 threading 模块本身的 API 将发生更改的情况下。
跟踪器中的问题 3042 提出,对于 Python 2.6,threading 模块将有两个 API - 当前的 API 和符合 PEP 8 的 API。当调用 -3 时,将发出有关即将删除原始 Java 风格 API 的警告。
在 Python 3000 中,threading API 将变得符合 PEP 8,这意味着 multiprocessing 模块和 threading 模块将再次具有匹配的 API。
时间/计划
有些人对今年 2.6 和 3.0 版本的此 PEP 的时间/延迟表示担忧,但是作者和其他一些人认为,此包提供的功能超过了包含的风险。
但是,考虑到不希望破坏 Python 核心,一些将 pyprocessing 的代码“重构”到 Python 核心的工作可以推迟到下一个 2.x/3.x 版本。这意味着对 Python 核心的实际风险最小,并且主要限制在包本身。
未解决的问题
- 确认没有“默认”远程连接功能,如果需要,为提供远程功能的那些类默认启用远程安全机制。
- 一些 API(例如
Queue
方法qsize()
、task_done()
和join()
)要么需要添加,要么需要明确识别并记录其排除的原因。
已解决的问题
- roudkerk 在 issue 1683 中提交的
PyGILState
错误修复程序必须应用于软件包单元测试才能正常工作。 - 现有的文档必须迁移到 ReST 格式。
- 依赖 ctypes:
pyprocessing
软件包对 ctypes 的依赖性阻止了该软件包在不支持 ctypes 的平台上运行。这不是此软件包的限制,而是 ctypes 的限制。 - 已完成:将顶级软件包从“pyprocessing”重命名为“multiprocessing”。
- 已完成:还要注意,进程生成的默认行为使其本身与在 IDLE 中使用不兼容,这将作为错误修复或“setExecutable”增强功能进行检查。
- 已完成:添加“multiprocessing.setExecutable()”方法以覆盖软件包的默认行为,从而使用当前可执行文件名而不是 Python 解释器来生成进程。请注意,Mark Hammond 建议为此使用工厂风格的接口 [7]。
参考文献
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0371.rst
上次修改时间: 2023-09-09 17:39:29 GMT