现在的笔记本电脑,台式机都流行多核心,低频率的架构,原因是低频率低耗电,而多核心又可以在并行计算中表现出色。在上一篇教程中:多多教Python:Python 基本功: 13. 多线程运算提速zhuanlan.zhihu.com
描述了如何用 Python 的多线程模块来实现计算提速,但是受限于在一个进程,也就是单核心上的运算。而这篇教程,将告诉大家如何利用 Python 在多核心上做并行计算。
教程需求:Mac OS (Windows, Linux 会略有不同)
安装了 Python 3.0 版本以上, PyCharm
阅读了多多教Python:Python 基本功: 6. 第一个完整的程序zhuanlan.zhihu.com多多教Python:Python 基本功: 10. 面对对象-类 Classzhuanlan.zhihu.com
多进程 vs. 多线程
多进程 (Multi-Process) 和多线程 (Multi-Thread) 最大的区别是,多进程是在各自单独的进程内存管理下运行代码,而多线程是共享一个进程内存。在各自单独的进程管理下,多进程的明显优势是可以最大的利用计算机多核心的处理能力。但是多进程也有其劣势,比如说在进程之间通信需要 IPC (Inter Process Communication) 工具,而不像多线程那样可以共享内存数据。
如果你拥有的是2015年之后的电脑,那么CPU基本上都是多核心的。从 多多教Python:Python 基本功: 0. 选择环境 开始跟随的小伙伴应该知道作者是在苹果电脑上开发 Python的,而现在你可以在苹果官网买到 8核心的笔记本:最高可配置 8核 的2019款 Mac 笔记本电脑
或者一台最高配置达到18核心的 Xeon 架构苹果台式机:iMac Pro 2018款, Xeon 架构的处理器
苹果电脑作为消费者机型,已经带来了最高 18核心的 Xeon 服务器架构的电脑,说明多核心计算能力已经普及到了大众。一旦你学习了如何利用 Python 来调度如此强大的计算能力,你就可以比别人更快的一步的获得重要的信息资源。
多进程库 Multiprocessing
通过调用 Python 自带的多进程库 Multiprocessing, 你就可以轻松的在本地电脑上进行多核并行计算,现在我们来看一些代码了解一下这个库:
import math
import datetime
import multiprocessing as mp
def train_on_parameter(name, param):
result = 0
for num in param:
result += math.sqrt(num * math.tanh(num) / math.log2(num) / math.log10(num))
return {name: result}
if __name__ == '__main__':
start_t = datetime.datetime.now()
num_cores = int(mp.cpu_count())
print("本地计算机有: " + str(num_cores) + " 核心")
pool = mp.Pool(num_cores)
param_dict = {'task1': list(range(10, 30000000)),
'task2': list(range(30000000, 60000000)),
'task3': list(range(60000000, 90000000)),
'task4': list(range(90000000, 120000000)),
'task5': list(range(120000000, 150000000)),
'task6': list(range(150000000, 180000000)),
'task7': list(range(180000000, 210000000)),
'task8': list(range(210000000, 240000000))}
results = [pool.apply_async(train_on_parameter, args=(name, param)) for name, param in param_dict.items()]
results = [p.get() for p in results]
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("多进程计算 共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")核心数量: cpu_count() 函数可以获得你的本地运行计算机的核心数量。如果你购买的是 Intel i7或者以上版本的芯片,你会得到一个乘以2的数字,得益于超线程 (Hyper-Threading) 结构,Python 可利用核心数量是真实数量的2倍!所以我在前文中会建议Python开发者购买 i7 而不是 第八代之前的 i5。
进程池: Pool() 函数创建了一个进程池类,用来管理多进程的生命周期和资源分配。这里进程池传入的参数是核心数量,意思是最多有多少个进程可以进行并行运算。
异步调度: apply_async() 是进程池的一个调度函数。第一个参数是计算函数,和 多多教Python:Python 基本功: 13. 多线程运算提速 里多线程计算教程里创建线程的参数 target 类似。第二个参数是需要传入计算函数的参数,这里传入了计算函数名字和计算调参。而异步的意义是在调度之后,虽然计算函数开始运行并且可能没有结束,异步调度都会返回一个临时结果,并且通过列表生成器 (参考: 多多教Python:Python 基本功: 12. 高纬运算的救星 Numpy) 临时的保存在一个列表里,这里就是 results。
调度结果: 如果你检查列表 results 里的类,你会发现 apply_async() 返回的是 ApplyResult,也就是调度结果类。这里用到了 Python 的异步功能,目前教程还没有讲到,简单的来说就是一个用来等待异步结果生成完毕的容器。
获取结果: 调度结果 ApplyResult 类可以调用函数 get(), 这是一个非异步函数,也就是说 get() 会等待计算函数处理完毕,并且返回结果。这里的结果就是计算函数的 return。
并行计算 Parallel Processing
在我们写完第一个多核计算 Python 文件后,就可以准备开始执行了。这里用的环境是 Mac OS, PyCharm IDE。作者首先在 Intel 第七代 4核 i7,超线程8核的 CPU 上运行:
本地计算机有: 8 核心
多进程计算 共消耗: 43.15 秒
Process finished with exit code 0
一共用了 43.15 秒,平均每一个任务用了 5.39 秒。在运行的时候如果查看 Activity Monitor:8 个 Python 独立进程同时运行
我们会发现在进程表里出现了8个 Python 程序,每一个都是一个线程,占用 CPU 比率 在 80-87% 之间。
下面我们再把同样的代码放在 Macbook Pro 上运行,用的是 Intel 第八代 i5 四核,超线程8核处理器:
本地计算机有: 8 核心
Process SpawnPoolWorker-2:
Process SpawnPoolWorker-3:
Process SpawnPoolWorker-4:
Process SpawnPoolWorker-5:
Process SpawnPoolWorker-6:
Process SpawnPoolWorker-7:
Process SpawnPoolWorker-1:
Process SpawnPoolWorker-8:
Traceback (most recent call last):
这里如果我们在运行的时候打断一下 (在 PyCharm 中点进进程终止),我们会发现8个进程也会被回收起来,这是因为我们的主进程在 PyCharm 内部会跟踪新生成的8个任务进程,一旦主进程收到了关闭/停止指令,8个任务进程也会同时被关闭。但是在其他情况下,我们需要检查是否所有进程都被正常停止,防止占用计算机资源。
本地计算机有: 8 核心
多线程计算 共消耗: 66.38 秒
Process finished with exit code 0
我们发现这里的耗时是 66.38秒,相比较于 i7 的处理速度降低了 53%。同时还因为这个计算函数需要占用大量的内存,所以内存的读写速度 (笔记本电脑通常是 1833 Mhz) 也有一定的影响。
资源共享 Resource Sharing
多线程因为共享一个进程的内存,所以在并行计算的时候会出现资源竞争的问题,这个在多多教Python:Python 基本功: 13. 多线程运算提速 已经提到过。而多进程虽然避免了这个问题,但是无法像多线程一样轻易的调用一个内存的资源。为了能让多进程之间进行通讯 (IPC),Python 的 Multiprocessing 库提供了几种方案: Pipe, Queue 和 Manager。这里 Pipe 我就直接引用一个外部我觉得很简单明了的介绍,Queue 有兴趣的小伙伴可以在教程结尾找到外部的链接,然后我会在之前的例子中加入 Manager。管道 Pipe:
Pipe可以是单向(half-duplex),也可以是双向(duplex)。我们通过mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。一个进程从PIPE一端输入对象,然后被PIPE另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。
————————————————
版权声明:本文为CSDN博主「gmHappy」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
import multiprocessing as mul
def proc1(pipe):
pipe.send('hello')
print('proc1 rec:', pipe.recv())
def proc2(pipe):
print('proc2 rec:', pipe.recv())
pipe.send('hello, too')
# Build a pipe
pipe = mul.Pipe()
if __name__ == '__main__':
# Pass an end of the pipe to process 1
p1 = mul.Process(target=proc1, args=(pipe[0],))
# Pass the other end of the pipe to process 2
p2 = mul.Process(target=proc2, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
#————————————————
#版权声明:本文为CSDN博主「gmHappy」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
#原文链接:https://blog.csdn.net/ctwy291314/article/details/89358144管理员 Manager
Manager 是一个 Multiprocessing 库里的类,用来创建可以进行多进程共享的数据容器,容器种类包括了几乎所有 Python 自带的数据类,详情可以参考: 多多教Python:Python 基本功: 3. 数据类型。这里我们把 Manager 加入到前文的例子中:
import math
import datetime
import multiprocessing as mp
def train_on_parameter(name, param, result_dict, result_lock):
result = 0
for num in param:
result += math.sqrt(num * math.tanh(num) / math.log2(num) / math.log10(num))
with result_lock:
result_dict[name] = result
return
if __name__ == '__main__':
start_t = datetime.datetime.now()
num_cores = int(mp.cpu_count())
print("本地计算机有: " + str(num_cores) + " 核心")
pool = mp.Pool(num_cores)
param_dict = {'task1': list(range(10, 30000000)),
'task2': list(range(30000000, 60000000)),
'task3': list(range(60000000, 90000000)),
'task4': list(range(90000000, 120000000)),
'task5': list(range(120000000, 150000000)),
'task6': list(range(150000000, 180000000)),
'task7': list(range(180000000, 210000000)),
'task8': list(range(210000000, 240000000))}
manager = mp.Manager()
managed_locker = manager.Lock()
managed_dict = manager.dict()
results = [pool.apply_async(train_on_parameter, args=(name, param, managed_dict, managed_locker)) for name, param in param_dict.items()]
results = [p.get() for p in results]
print(managed_dict)
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("多线程计算 共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")
这里我们用 Manager 来创建一个可以进行进程共享的字典类,随后作为第三个参数传入计算函数中。计算函数把计算好的结果保存在字典里,而不是直接返回。在并行运算结束之后,我们通过 print() 函数来查看字典里的结果。注意这里既然出现了可以共享的数据类,我们就要再次通过锁 (Lock) 来避免资源竞争,所以同时通过 Manager 创建了锁 Lock 类,以第四个参数传入计算函数,并且用 With 语境来锁住共享的字典类。
小结:
并行运算可以最大化的利用当代的计算能力,把原本需要几个小时,几天的处理任务变成几分钟,几小时。而通过 Python 已有的 Multiprocessing 库,几行代码就可以把你手上的计算器变成一台超级计算机。而如果你手上的计算器无法满足你的计算需求,你可以借助云,在云服务器上轻松的租凭一台 Xeon 架构的超级计算器帮你完成任务。有兴趣的小伙伴可以继续阅读一些外部的教程:
本文引用的 Pipe, Queue 教程:Python multiprocessing使用详解blog.csdn.net
一篇简书上的多进程教程:Python3多进程multiprocessing模块的使用www.jianshu.com