- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
第9章 multiprocessing 模块
读完本章之后你将能够回答下列问题
multiprocessing模块提供了什么?
进程和线程的区别是什么?
我该如何选择合适大小的进程池?
我该如何使用非持久队列来处理工作?
进程间通信的代价和好处是什么?
我该如何用多CPU来处理numpy数据?
为什么我需要加锁来避免数据丢失
CPython默认没有使用多CPU。一部分原因是Python是被设计用于单核领域,另一部分原因是实际上有效的并行化是相当困难的。Python给我们提供了工具,但是让我们自己来做出选择。然而,看到你的多核机器上只使用了一个CPU来长期运行一个进程是痛苦的,所以在本章中我们会立即检视各种方法来使用所有的机器核。
备忘
值得注意的是我们在上面提到的是CPython(我们所有人都使用的通用实现)。在Python语言中,没有什么东西阻止使用多核系统。CPython的实现不能有效使用多核,但是其他实现(例如,具有即将到来的软件事务内存的PyPy)可能不会被这个约束所束缚。
我们生活于一个多核世界中——笔记本电脑上普遍为4核,桌面电脑上的8核的配置将很快流行起来,并且10-、12-和15核CPU的服务器也存在。如果你的工作能被拆分成运行于多核CPU,而又不花费太多工程方面的努力,那么这是一个要考虑的明智的方向。
当习惯于在一个CPU集上并行化问题时,你就能期待用n核达到n倍(nx)的速度提升。如果你有一个4核的机器,并且能为你的任务使用全部的4核,它就有可能以原来运行时间的四分之一来跑完。你不可能看到一个大于4倍的提速。在实践中,你可能会看到3到4倍的增益。
每一个额外的处理将会增加通信的开销和减少可使用的内存,所以你很少会得到一个完全的n倍的提速。取决于你正在解决的问题,通信开销甚至可以变得很大以致于你能够看到很明显的减速。这些类型的问题常常是存在于任何类型的并行编程中的复杂性,并且通常需要去改变算法。这就是并行编程常常被认为是一种艺术的原因。
如果你不熟悉Amdahl定律,那就值得去阅读一些背景材料。这个定律揭示了如果你的代码只有一小部分能够并行化,那就和你给它用多少CPU无关。整体上,它还是无法运行得更快。在你得到回报减弱的要点之前,即使你的程序在运行时有很大一部分能够并行化,也只有有限数量的CPU能被有效利用来使整体进程运行得更快。
multiprocessing模块让你使用基于进程和基于线程的并行处理,在队列上共享任务,以及在进程间共享数据。它主要是集中于单机多核的并行(对多机并行来说,有更好的选择)。一个很普遍的用法就是针对CPU密集型的问题,在一个进程集上并行化一个任务。你可能也用它来并行化I/O密集型问题,但是就如我们在第8章所见的那样,有更好的工具来处理这类问题(例如,在Python 3.4+中的新asyncio模块和在Python 2+中的gevent或者tornado)。
备忘
OpenMP是一个低层的多核接口——你可能想知道是集中精力于它上面还是于multiprocessing上面。我们在第7章中与Cython和Pythran在一起介绍过它,但是我们在第7章中并没有全面涉猎它。multiprocessing在一个更高的层次上工作,共享Python的数据结构,而OpenMP一旦被编译成C后,就使用C的原生对象(例如,整型数和浮点数)来工作。它只有在你编译你的代码时才有意义去使用。如果你不去编译(例如,如果你正使用高效的numpy代码并想要在多核上运行),那么坚持使用multiprocessing可能是正确的途径。
为了并行化你的任务,你必须要以比编写一个串行程序的普通方式稍微有别一点的方式去思考。你也必须接受更大的困难去调试一个并行任务——它常常是很令人沮丧的。我们要推荐尽可能地让并行保持简单(即使你压榨不出你的机器的每一滴最后的力量),这样你就会保持高速的开发。
一个特别困难的主题就是在并行系统中共享状态——凭感觉这好似应该简单,但是却带来了很多开销,并且难以做正确。有许多应用案例,每一个都有不同的妥协,所以肯定没有针对所有情况的解决方案。在9.5节,我们将会用一只眼盯着同步的开销来遍历下状态共享。避免共享状态会让你的生活变得简单很多。
事实上,一个算法能够几乎全凭有多少状态必须要共享来分析出它在并行环境中表现如何。例如,如果我们有多个Python进程全都是解决一样的问题,而没有彼此间相互通信(一种已知为窘迫并行的情况),那当我们增加越来越多的Python进程时,就不会招致多大的惩罚。
另一方面,如果每一个进程需要和所有其他Python进程来通信,那么通信开销将会慢慢让处理变得不堪重负,拖慢了事情。这意味着当我们增加越来越多的Python进程时,我们实际上减慢了整体性能。
作为结果,有时必须要做一些反直觉的算法改动来有效解决并行问题。例如,当解决并行扩散方程(第6章)时,每一个进程实际上做了另一个进程也在做的冗余工作。这种冗余降低了所需的通信量,并且提高了整体的计算速度!
multiprocessing模块有一些典型的工作:
用进程或池对象来并行化一个CPU密集型任务。
用哑元模块(奇怪的称呼)在线程池中并行化一个I/O密集型任务。
由队列来共享捎带的工作。
在并行工作者之间共享状态,包括字节、原生数据类型、字典和列表。
如果你从一种使用线程来做CPU密集型任务(例如,C++或Java)的语言中转过来,那么你应该知道尽管在Python中的线程是OS原生的(它们不是模拟出来的,它们是真实的操作系统线程),它们被全局解释锁(GIL)所束缚,所以同一个时刻只有一个线程可以和Python对象交互。
通过使用进程,我们并行运行了一定数量的Python解释器,每一个进程都有私有的内存空间,有自己的GIL,并且每一个都串行运行(所以没有GIL之间的竞争)。这是在Python中提升CPU密集型任务速度的最简单的方式。如果我们需要共享状态,那么我们就需要增加一些通信开销。我们在9.5节中会进行探索。
如果你用numpy数组工作,你可能想知道你是否可以创建一个更大的数组(例如,一个大2维矩阵),以及是否可以让进程并行工作于分段数组。你可以,但是通过试错难以发现怎样做,所以在9.6节中,我们会经历一遍在4个CPU之间共享一个6.4GB的numpy数组。与其传送部分拷贝数据(至少会让在RAM中的工作集大小翻倍,并且会产生巨大的通信开销),我们在进程间共享底层的数组字节。这是一个在一台机器上的本地工作者之间共享大数组的理想方式。
备忘
这里,我们是在基于*nix的机器上讨论multiprocessing(本章是用ubuntu来写的,代码应该能不做改动在Mac上运行)。对于Windows机器,你应该检查官方文档。
在本章接下来中,我们会硬编码一定数量的进程(NUM_PROCESSES=4)来在Ian笔记本上匹配4个物理核。默认情况下,multiprocessing将使用它能见到的尽可能多的核(机器有8核——4 CPU和4超线程)。通常你会避免硬编码进程的数量来创建,除非你有特别的要求来管理你的资源。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论