为什么,何时以及如何在Python中使用多线程和多处理

敬礼,哈布罗夫斯克。目前,OTUS已为机器学习课程开放了一套课程,与此相关的是,我们为您翻译了一个非常有趣的“童话”。走。




从前,在一个遥远的星系中……一个

聪明而强大的巫师生活在沙漠中间的一个小村庄。他们的名字叫邓布达夫。他不仅睿智而强大,而且还帮助来自遥远土地的人们向巫师寻求帮助。我们的故事始于一位旅行者为巫师带来了神奇的卷轴。旅行者不知道卷轴里有什么,他只知道如果有人能揭开卷轴的所有秘密,那就是邓布达夫。

第1章:单线程,单进程


如果您还没有猜到,我就对处理器及其功能进行类比。我们的向导是一个处理器,滚动是一个链接列表,这些链接可导致Python掌握它的能力和知识。

一个向导毫不费力地破译清单的向导的第一个念头就是将他忠实的朋友(加里戈恩?我知道,我知道这听起来很糟糕)送到画卷中描述的每个地方,以便找到并带回他可以在那里找到的东西。

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 µs, total: 135 ms
Wall time: 12.3 s
In [ ]:

如您所见,我们只需使用for循环逐个迭代URL并读取答案。多亏了%%的时间IPython的魔力,我们可以看到我悲伤的互联网花费了大约12秒的时间。

第2章:多线程


并非没有道理,巫师以他的智慧而闻名;他很快就能想出一种更有效的方法。与其将一个人按顺序送到每个地方,不如不聚集一群可靠的同伙并同时将他们发送到世界的不同地方!该向导将能够将他们带来的所有知识统一起来!

没错,我们可以使用多线程一次访问多个URL ,而不必按顺序循环查看列表

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

好多了!几乎像...魔术。使用多个线程可以大大加快许多I / O任务。以我为例,花费在阅读URL上的大部分时间是由于网络延迟。您猜对了,与I / O关联的程序会花费大部分时间等待输入或输出(就像向导在等待其朋友从滚动条移到其他位置并返回)一样。可以从网络,数据库,文件或用户输入/输出。这样的I / O通常会花费很多时间,因为源可能需要在将数据传输到I / O之前执行预处理。例如,处理器的计数速度将比网络连接传输数据的速度快得多(速度约为例如Flash与您的祖母)。

注意multithreading在清理网页等任务中非常有用。

第三章:多处理


多年过去了,好巫师的名声在增长,随之而来的是一个公正的黑暗巫师的嫉妒(Sarumort?或者Volandeman?)。这位黑巫师手持不可估量的狡猾,羡慕不已,对邓布达夫施加了可怕的诅咒。当诅咒超越了他时,邓布利夫意识到他只有片刻可以挡住他。绝望的他翻遍了他的咒语书,很快找到了一个应该起作用的反咒语。唯一的问题是向导需要计算所有小于1,000,000的素数之和,当然,这是一个奇怪的咒语,但我们拥有的咒语。

巫师知道,如果他有足够的时间来计算价值,那将是微不足道的,但是他没有那么奢侈。尽管他是一个伟大的巫师,但他仍然受到人性的限制,一次只能检查一个数字。如果他决定简单地将素数相加,那将花费太多时间。在应用反咒语之前只剩几秒钟的时候,他突然想起了多处理法术,这是他多年前从魔术卷轴中学到的。此咒语将使他能够复制自己,以便在副本之间分配数字并同时检查多个副本。最后,他只需要简单地将他和他的副本会发现的数字相加即可。

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

现代处理器具有多个核心,因此我们可以使用多进程处理模块multiprocessing来加速任务与处理器相关联的任务是程序,它们在大多数时间都在处理器上执行计算(简单的数学计算,图像处理等)。如果计算可以彼此独立进行,我们就有机会在可用处理器内核之间进行划分,从而显着提高处理速度。

您要做的就是:

  1. 确定要应用的功能。
  2. 准备要应用该功能的元素列表;
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

注意:可以定义一个函数来执行可以并行执行的任何任务。例如,一个函数可能包含将计算结果写入文件的代码。

那么为什么我们需要分开multiprocessingmultithreading?如果您曾经尝试使用多线程来提高处理器上任务的性能,那么结果恰恰相反。那太可怕了!让我们看看它是怎么发生的。

正如向导本身受人性的限制并且每单位时间只能计算一个数字一样,Python附带了一个称为全局解释器锁定(GIL)的东西。 Python很乐意让您产生所需数量的线程,但是 GIL确保在任何给定时间仅执行这些线程之一。

对于与I / O相关的任务,这种情况是完全正常的。一个线程将请求发送到一个URL并等待响应,然后才能将该线程替换为另一个线程,这会将另一个请求发送到另一个URL。由于线程在收到响应之前不需要执行任何操作,因此在给定时间仅运行一个线程没有任何区别。

对于在处理器上执行的任务,拥有多个线程几乎与装甲中的乳头一样没用。由于每单位时间只能执行一个线程,因此即使您生成多个线程,每个线程都会分配一个数字以进行简单性检查,所以处理器仍然只能使用一个线程。实际上,这些数字仍将一一检查。使用多个线程的开销将有助于降低性能,您可以multithreading在处理器上执行的任务中观察到这一点。

为了避免这种“限制”,我们使用了multiprocessing。正如您所说,多处理使用了多个进程,而不是使用线程。每个进程都有自己的个人解释器和内存空间,因此GIL不会限制您。实际上,每个进程都将使用其自己的处理器内核并以其自己的唯一编号工作,并且这将与其他进程的工作同时执行。他们多么甜蜜!

您可能会注意到,multiprocessing与常规for循环甚至相比,使用CPU时的负载会更高multithreading。发生这种情况是因为您的程序不是使用一个内核,而是使用多个内核。这很好!

记住,multiprocessing它有自己的管理多个进程的开销,通常比多线程的开销要严重得多。 (多处理产生单独的解释器,并为每个进程分配其自己的内存区域,所以是的!)也就是说,通常,当您想以这种方式退出时最好使用轻量级的多线程版本(记住与I / O相关的任务)。但是当处理器上的计算成为瓶颈时,模块时间就到了multiprocessing。但是请记住,强大的力量伴随着巨大的责任。

如果产生的进程多于处理器每单位时间可处理的进程,则您会注意到性能将开始下降。发生这种情况的原因是,由于存在更多的进程,因此操作系统需要通过改组处理器内核之间的进程来做更多的工作。实际上,一切可能比我今天告诉你的还要复杂,但是我传达了主要思想。例如,在我的系统上,当进程数为16时,性能将下降。这是因为处理器中只有16个逻辑核心。

第四章:结论


  • 在与I / O相关的任务中,它multithreading可以提高性能。
  • 在与I / O相关的任务中,multiprocessing它也可以提高生产率,但是通常来说,成本比使用时要高multithreading
  • Python GIL的存在使我们很清楚,程序中的任何给定时间只能执行一个线程。
  • 在与处理器有关的任务中,使用multithreading会降低性能。
  • 在与处理器相关的任务中,使用multiprocessing可以提高性能。
  • 奇才很棒!

这就是我们将结束我们的介绍multithreading,并multiprocessing在Python 今天现在去赢!



“使用图形分析和解析开放数据来建模COVID-19。” 免费课程。

All Articles