首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

如何利用并发性加速你的 python程序(下)

AI 科技评论按,本文是工程师 Jim Anderson 分享的关于「通过并发性加快 python 程序的速度」的文章的下篇,主要内容是 CPU 绑定程序加速相关。

在上篇中,我们已经讲过了相关概念以及I/O 绑定程序的加速。下篇的内容是 CPU 程序加速。AI 科技评论编译整理如下:

如何加速 CPU 绑定程序

到目前为止,前面的例子都处理了一个 I/O 绑定问题。现在,你将研究 CPU 绑定的问题。如你所见,I/O 绑定的问题大部分时间都在等待外部操作(如网络调用)完成。另一方面,CPU 限制的问题只执行很少的 I/O 操作,它的总体执行时间取决于它处理所需数据的速度。

在我们的示例中,我们将使用一个有点愚蠢的函数来创建一些需要在 CPU 上运行很长时间的东西。此函数计算从 0 到传入值的每个数字的平方和:

你将处理一大批数据,所以这需要一段时间。记住,这只是代码的一个占位符,它实际上做了一些有用的事情,需要大量的处理时间,例如计算公式的根或对大型数据结构进行排序。

CPU 绑定的同步版本

现在让我们看一下这个示例的非并发版本:

import time

def cpu_bound(number):

return sum(i * i for i in range(number))

def find_sums(numbers):

for number in numbers:

cpu_bound(number)

if __name__ == "__main__":

numbers = [5_000_000 + x for x in range(20)]

start_time = time.time()

find_sums(numbers)

duration = time.time() - start_time

print(f"Duration seconds")

此代码调用 cpu_bound() 20 次,每次使用不同的大数字。它在单个 CPU 上单个进程中的单个线程上完成所有这些工作。执行时序图如下:

与 I/O 绑定示例不同,CPU 绑定示例的运行时间通常相当一致。这台机器大约需要 7.8 秒:

显然我们可以做得更好。这都是在没有并发性的单个 CPU 上运行的。让我们看看我们能做些什么来改善它。

线程和异步版本

你认为使用线程或异步重写此代码会加快速度吗?

如果你回答「一点也不」,这是有道理的。如果你回答,「它会减慢速度,」那就更对啦。

原因如下:在上面的 I/O 绑定示例中,大部分时间都花在等待缓慢的操作完成上。线程和异步通过允许你重叠等待的时间而不是按顺序执行,这能加快速度。

但是,在 CPU 绑定的问题上,不需要等待。CPU 会尽可能快速地启动以解决问题。在 python 中,线程和任务都在同一进程中的同一个 CPU 上运行。这意味着一个 CPU 不仅做了非并发代码的所有工作,还需要做线程或任务的额外工作。它花费的时间超过 10 秒:

我已经编写了这个代码的线程版本,并将它与其他示例代码放在 Github repo 中,这样你就可以自己测试它了。

CPU 绑定的多处理版本

现在,你终于要接触多处理真正与众不同的地方啦。与其他并发库不同,多处理被显式设计为跨多个 CPU 共同承担工作负载。它的执行时序图如下所示:

它的代码是这样的:

import multiprocessing

import time

def cpu_bound(number):

return sum(i * i for i in range(number))

def find_sums(numbers):

with multiprocessing.Pool() as pool:

pool.map(cpu_bound, numbers)

if __name__ == "__main__":

numbers = [5_000_000 + x for x in range(20)]

start_time = time.time()

find_sums(numbers)

duration = time.time() - start_time

print(f"Duration seconds")

这些代码和非并发版本相比几乎没有要更改的。你必须导入多处理,然后把数字循环改为创建多处理.pool 对象,并使用其.map()方法在工作进程空闲时将单个数字发送给它们。

这正是你为 I/O 绑定的多处理代码所做的,但是这里你不需要担心会话对象。

如上所述,处理 multiprocessing.pool()构造函数的可选参数值得注意。可以指定要在池中创建和管理的进程对象的数量。默认情况下,它将确定机器中有多少 CPU,并为每个 CPU 创建一个进程。虽然这对于我们的简单示例来说很有用,但你可能希望在生产环境它也能发挥作用。

另外,和我们在第一节中提到的线程一样,multiprocessing.Pool 的代码是建立在 Queue 和 Semaphore 上的,这对于使用其他语言执行多线程和多处理代码的人来说是很熟悉的。

为什么多处理版本很重要

这个例子的多处理版本非常好,因为它相对容易设置,并且只需要很少的额外代码。它还充分利用了计算机中的 CPU 资源。在我的机器上,运行它只需要 2.5 秒:

这比我们看到的其他方法要好得多。

多处理版本的问题

使用多处理有一些缺点。在这个简单的例子中,这些缺点并没有显露出来,但是将你的问题分解开来,以便每个处理器都能独立工作有时是很困难的。此外,许多解决方案需要在流程之间进行更多的通信,这相比非并发程序来说会复杂得多。

何时使用并发性

首先,你应该判断是否应该使用并发模块。虽然这里的示例使每个库看起来非常简单,但并发性总是伴随着额外的复杂性,并且常常会导致难以找到的错误。

坚持添加并发性,直到出现已知的性能问题,然后确定需要哪种类型的并发性。正如 DonaldKnuth 所说,「过早的优化是编程中所有灾难(或者至少大部分灾难)的根源(Premature optimization is the root of all evil (or at least most of it) in programming)」。

一旦你决定优化你的程序,弄清楚你的程序是 CPU 绑定的还是 I/O 绑定的,这就是下一步要做的事情。记住,I/O 绑定的程序是那些花费大部分时间等待事情完成的程序,而 CPU 绑定的程序则尽可能快地处理数据。

正如你所看到的,CPU 绑定的问题实际上只有在使用多处理才能解决。线程和异步根本没有帮助解决这类问题。

对于 I/O 绑定的问题,python 社区中有一个通用的经验规则:「可以使用异步,必须使用线程。」异步可以为这种类型的程序提供最佳的速度,但有时需要某些关键库来利用它。记住,任何不放弃对事件循环控制的任务都将阻塞所有其他任务。

CPU 绑定加速的内容就到此为止啦,了解更多请访问原文!

https://realpython.com/python-concurrency/

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190202A0VGPX00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券