图片来自 Pixabay 的 Parker_West
很久以前,在一个遥远的星系里…
一个聪明而强大的巫师住在一个偏僻的小村庄里。让我们叫他 Dumbledalf。他不仅智慧和强大,而且他也乐于帮助任何请求帮助的人,这意味着人们会从四面八方向巫师寻求帮助。我们的故事开始于一个晴朗的日子,一个年轻的旅行者带给巫师一个魔法卷轴。旅行者不知道卷轴上有什么,但他知道如果有人能破译卷轴上的秘密,那一定是伟大的巫师丹布尔达夫。
如果你还没有猜到,我的这个相当伤感的比喻是在谈论 CPU 及其功能。我们的向导是 CPU,而神奇的卷轴是一系列的URL,它引导出 Python 的力量和运用这种力量的知识。
巫师毫不费力地破译了卷轴,他的第一个想法是派他信任的朋友*(哈拉贡?我知道,我知道,那太可怕了)*到卷轴上给出的每一个位置去看看,把他能找到的东西带回来。
如您所见,我们只是使用一个for loop
一个接一个地缓慢通过*URL 并读取响应。感谢%%time
IPython 的魔力,*我们可以看到,在我糟糕的互联网上,它只需要 12 秒。
巫师的智慧在这片土地上广为人知,他很快就想出了一个更有效的方法。与其按顺序派一个人去每个地点,为什么不召集一群(值得信赖的)人,同时分别派他们去每个地点,!一旦他们都回来了,巫师可以简单地组合他们带来的所有东西。
没错,我们可以使用multithreading
同时访问多个URL,而不是逐个遍历列表。
好多了!几乎像..魔法。使用多线程可以显著提高许多 IO 绑定 任务的速度。这里,读取URL所花费的大部分时间是由于网络延迟。 IO 绑定 程序大部分时间都在等待,你猜对了,输入/输出*(类似于向导需要等待他的一个/多个朋友去卷轴中给定的位置然后回来)*。这可能是来自网络、数据库、文件甚至用户的 I/O。这种 I/O 往往会花费大量的时间,因为源本身可能需要在传递 I/O 之前执行自己的处理。例如,CPU 的工作速度比网络连接传送数据的速度快得多(想想 Flash 与您的祖母)。
注意: *Multithreading*
在网页抓取这样的任务中非常有用。
随着岁月的流逝,我们巫师的名声越来越大,一个相当令人讨厌的黑巫师(萨鲁铎)也受到了嫉妒?沃尔德曼。).在狡猾和嫉妒的驱使下,黑巫师对邓布利达尔夫施了一个可怕的诅咒。一旦诅咒被解除,丹布尔达尔夫知道他只有很短的时间来打破它。绝望中,他翻看了他的咒语书,发现了一个看起来可能有用的反咒语。唯一的问题是,它要求他计算 1000000 以下所有质数的总和。奇怪的咒语,但事实就是如此。
现在,向导知道如果有足够的时间,计算值将是微不足道的,但时间对他来说不是奢侈品。尽管他是个巫师,但他也受到人性的限制,一次只能计算一个数字。如果他要一个一个地把质数加起来,那要花太长时间了。在逆转诅咒还剩几秒钟的时候,他突然想起了几年前从魔法卷轴中学到的multiprocessing
咒语。这个咒语可以让他复制自己,并且把复制的数字分开可以让他同时检查多个数字是否是质数。最后,他要做的就是把他和他的复制品发现的所有质数加起来。
由于现代 CPU 通常拥有不止一个内核,我们可以通过使用multiprocessing
模块来加速 CPU 绑定的 任务。 CPU 受限 任务是将大部分时间花在 CPU 中执行计算(数学计算、图像处理等)的程序。).如果计算可以彼此独立地执行,我们可以在可用的 CPU 内核之间进行分配,从而显著提高处理速度。
你要做的就是:
- 定义要应用的功能
- 准备要应用该功能的项目列表
- 使用
multiprocessing.Pool
生成进程。传递给Pool()
的数字将是产生的进程数。嵌入在一个with
语句中确保了进程在完成执行后被终止。 - 使用池进程的
map
函数组合输出。map
功能的输入是应用于每个项目的功能和项目列表。
注意:可以定义函数,以便执行任何可以并行完成的任务。例如,该函数可能包含将计算结果写入文件的代码。
那么,为什么我们需要分开的multiprocessing
和multithreading
?如果你试图使用multithreading
来提高一个 CPU 绑定的 任务的性能,你可能会注意到你实际得到的是一个 性能下降 。异端!让我们看看为什么会这样。
就像巫师被他的人性所限制,一次只能计算一个数字一样,Python 自带了一个叫做全局解释器锁(GIL)的东西。 Python 会很乐意让你产生尽可能多的threads
,但是 GIL 确保在任何给定的时间,那些threads
中只有一个会被执行。
对于一个与 IO 相关的任务来说,这完全没问题。一个thread
向一个 URL 发出请求,当它等待响应时,那个thread
可以被另一个thread
替换,后者向另一个 URL 发出另一个请求。因为在收到响应之前thread
不需要做任何事情,所以在给定时间只有一个thread
在执行并不重要。
对于一个受 CPU 限制的 任务来说,拥有多个threads
就像胸甲上的乳头一样有用。因为一次只有一个thread
被执行,即使你产生了多个threads
,每个都有自己的数来检查质数*,CPU 仍然一次只处理一个thread
。实际上,这些数字仍然会被一个接一个地检查。如果在 CPU 受限的*任务中使用multithreading
,处理多个threads
的开销会导致性能下降。**
为了避开这个“限制”,我们使用了multiprocessing
模块。multiprocessing
不使用threads
,而是使用多个processes
。每个process
都有自己的解释器和内存空间,所以 GIL 不会有所保留。本质上,每个process
使用不同的 CPU 内核同时处理不同的数字。甜!
您可能会注意到,与使用简单的 for 循环甚至是multithreading
相比,使用multiprocessing
时 CPU 的利用率要高得多。这是因为您的程序使用了多个 CPU 内核,而不仅仅是一个内核。这是好事!
请记住,multiprocessing
有自己的管理多个processes
的开销,这通常比multithreading
的开销更大。(Multiprocessing
衍生出一个单独的解释器,给每个process
分配一个单独的内存空间,所以咄!).这意味着,根据经验,如果可以的话,最好使用轻量级的multithreading
(阅读: IO 绑定的 任务)。当 CPU 处理成为你的瓶颈时,一般就是召唤multiprocessing
模块的时候了。但是请记住,权力越大,责任越大。
如果你产生的processes
超过了你的 CPU 一次可以处理的数量,你会发现你的性能开始下降。这是因为操作系统现在必须做更多的工作来交换进出 CPU 内核,因为你有更多的内核。现实可能比简单的解释更复杂,但这是基本的想法。当我们到达 16 processes
时,您可以在我的系统上看到性能下降。这是因为我的 CPU 只有 16 个逻辑核心。
- 对于 IO 绑定的 任务,使用
multithreading
可以提高性能。 - 对于 IO 绑定的 任务,使用
multiprocessing
也能提高性能,但开销往往比使用multithreading
高。 - Python GIL 意味着在一个 Python 程序中,任何给定时间只能执行一个
thread
。 - 对于 CPU 绑定的 任务,使用
multithreading
实际上会恶化性能。 - 对于 CPU 绑定 的任务,使用
multiprocessing
可以提高性能。 - 巫师太牛逼了!
对 Python 中的multithreading
和multiprocessing
的介绍到此结束。勇往直前去征服吧!