PEP 371 – 将多进程包添加到标准库
- 作者:
- 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 核 Intel Xeon CPU @ 3.00GHz
- 16 GB 内存
- 在 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
如您所见,在这次 I/O 操作中,pyprocessing 仍然比使用多个线程更快。而使用多个线程比单线程执行本身更慢。
最后,我们将运行一个基于 socket 的测试,以展示网络 I/O 性能。此函数从局域网上的服务器获取一个 URL,该 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/ 目录中找到。这些示例将包含在包的文档中。
维护
Richard M. Oudkerk - pyprocessing 包的作者已同意在 Python SVN 中维护该包。Jesse Noller 也自愿帮助维护/文档和测试该包。
API 命名
虽然该包的 API 的目标是紧密模仿 Python 2.x 中 threading 和 Queue 模块的 API,但这些模块不符合 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。
时机/日程
有人对本 PEP 在今年 2.6 和 3.0 版本发布的时间/迟到提出了担忧,但作者和其他人认为该包提供的功能超越了包含的风险。
然而,考虑到不破坏 Python 核心的愿望,pyprocessing 代码的某些重构“进入”Python 核心可以推迟到下一个 2.x/3.x 版本。这意味着 Python 核心的实际风险很小,并且主要局限于实际包本身。
未解决的问题
- 确认没有“默认”远程连接功能,如果需要,默认启用提供远程功能的类的远程安全机制。
- 某些 API(
Queue方法qsize()、task_done()和join())要么需要添加,要么需要明确识别并记录其排除的原因。
已关闭的问题
- roudkerk 在问题 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