Skip to content

Latest commit

 

History

History
539 lines (373 loc) · 28 KB

File metadata and controls

539 lines (373 loc) · 28 KB

十三、多进程——当单个 CPU 核心不够时

在上一章中,我们讨论了影响性能的因素和一些提高性能的方法。本章实际上可以看作是性能提示列表的扩展。在本章中,我们将讨论多进程模块,该模块使您的代码在多个 CPU 核甚至多台机器上运行变得非常容易。这是一种简单的方法,可以绕过在上一章中讨论过的全局解释器锁GIL)。

总而言之,本章将涵盖:

  • 局部多进程
  • 远程多进程
  • 进程之间的数据共享和同步

多线程与多进程

在这本书中,我们还没有真正介绍多线程,但您可能在过去见过多线程代码。多线程和多进程之间的最大区别在于,使用多线程时,所有内容仍然在单个进程中执行。这有效地将性能限制在单个 CPU 核心上。它实际上会进一步限制您,因为代码必须处理 CPython 的 GIL 限制。

GIL 是 Python 用于安全内存访问的全局锁。关于性能,第 12 章性能–跟踪并减少内存和 CPU 使用将对其进行更详细的讨论。

为了说明多线程代码并不能在所有情况下都有助于提高性能,实际上可能比单线程代码稍慢,请看以下示例:

import datetime
import threading

def busy_wait(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    n = 10000000
    start = datetime.datetime.now()
    for _ in range(4):
        busy_wait(n)
    end = datetime.datetime.now()
    print('The single threaded loops took: %s' % (end - start))

    start = datetime.datetime.now()
    threads = []
    for _ in range(4):
        thread = threading.Thread(target=busy_wait, args=(n,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()

    end = datetime.datetime.now()
    print('The multithreaded loops took: %s' % (end - start))

对于 Python 3.5,它具有新的和改进的 GIL 实现(在 Python 3.2 中引入),性能相当不错,但没有任何改进:

# python3 test_multithreading.py
The single threaded loops took: 0:00:02.623443
The multithreaded loops took: 0:00:02.597900

Python 2.7 仍然使用旧的 GIL,单线程变体的性能要好得多:

# python2 test_multithreading.py
The single threaded loops took: 0:00:02.010967
The multithreaded loops took: 0:00:03.924950

从这个测试中,我们可以得出结论,Python 2 在某些情况下更快,而 Python 3 在其他情况下更快。您应该从中得到的是,没有任何性能理由在 Python2 或 Python3 之间进行选择。请注意,在大多数情况下,Python3 至少与 Python2 一样快,如果不是这样,它很快就会被修复。

无论如何,对于 CPU 绑定的操作,线程并没有提供任何性能优势,因为它在单处理器内核上执行。然而,对于 I/O 绑定的操作,threading库确实提供了明显的好处,但在这种情况下,我建议改为尝试asynciothreading最大的问题是,如果其中一个线程阻塞,则主进程将阻塞。

multiprocessing库提供了一个与threading库非常相似的 API,但它使用了多个进程而不是多个线程。其优点是 GIL 不再是一个问题,可以使用多进程器核心甚至多台机器进行处理。

为了说明性能差异,让我们在使用multiprocessing模块而不是threading时重复测试:

import datetime
import multiprocessing

def busy_wait(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    n = 10000000
    start = datetime.datetime.now()

    processes = []
    for _ in range(4):
        process = multiprocessing.Process(
            target=busy_wait, args=(n,))
        process.start()
        processes.append(process)

    for process in processes:
        process.join()

    end = datetime.datetime.now()
    print('The multiprocessed loops took: %s' % (end - start))

在运行时,我们看到了巨大的改进:

# python3 test_multiprocessing.py
The multiprocessed loops took: 0:00:00.671249

请注意,这是在四核处理器上运行的,这就是我选择四个进程的原因。multiprocessing库默认为multiprocessing.cpu_count(),统计可用的 CPU 内核,但该方法没有考虑 CPU 超线程。这意味着在我的例子中它将返回 8,这就是为什么我将它硬编码为 4。

需要注意的是,由于multiprocessing库使用多个进程,因此需要从子进程导入代码。结果是multiprocessing库在 Python 或 IPython shell 中不起作用。正如我们将在本章后面看到的,IPython 对多进程有自己的规定。

超线程与物理 CPU 内核

在大多数情况下,超线程非常有用,可以提高性能,但当您真正最大化 CPU 使用率时,通常最好只使用物理处理器计数。为了演示这如何影响性能,我们将再次运行上一节中的测试。这一次,我们将使用 1、2、4、8 和 16 个流程来演示它如何影响性能。幸运的是,multiprocessing库有一个很好的Pool类为我们管理流程:

import sys
import datetime
import multiprocessing

def busy_wait(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    n = 10000000
    start = datetime.datetime.now()
    if sys.argv[-1].isdigit():
        processes = int(sys.argv[-1])
    else:
        print('Please specify the number of processes')
        print('Example: %s 4' % ' '.join(sys.argv))
        sys.exit(1)

    with multiprocessing.Pool(processes=processes) as pool:
        # Execute the busy_wait function 8 times with parameter n
        pool.map(busy_wait, [n for _ in range(8)])

    end = datetime.datetime.now()
    print('The multithreaded loops took: %s' % (end - start))

池代码使得启动工人池和处理队列变得更简单。在本例中,我们使用了map,但还有几个其他选项,如imapmap_asyncimap_unorderedapplyapply_asyncstarmapstarmap_async。由于这些方法与类似命名的itertools方法的工作原理非常相似,因此不会有所有这些方法的具体示例。

但现在,使用不同数量的过程进行的测试:

# python3 test_multiprocessing.py 1
The multithreaded loops took: 0:00:05.297707
# python3 test_multiprocessing.py 2
The multithreaded loops took: 0:00:02.701344
# python3 test_multiprocessing.py 4
The multithreaded loops took: 0:00:01.477845
# python3 test_multiprocessing.py 8
The multithreaded loops took: 0:00:01.579218
# python3 test_multiprocessing.py 16
The multithreaded loops took: 0:00:01.595239

您可能没有预料到这些结果,但这正是超线程的问题所在。一旦单个进程实际使用 100%的 CPU 核心,进程之间的任务切换实际上会降低性能。由于只有4物理内核,其他4必须努力在处理器内核上完成一些事情。这场战斗需要时间,这就是为什么4流程版本比8流程版本稍快的原因。此外,在使用12内核的运行中也可以看到调度效果。如果我们看一下单核版本,我们会看到它花费了5.3秒,这意味着4核应该在5.3 / 4 = 1.325秒内完成,而不是实际花费的1.48秒。2核心版本也有类似的效果,2.7 / 2 = 1.35秒仍然比4核心版本快。

如果您确实因为 CPU 受限的问题而面临性能压力,那么匹配物理 CPU 内核是最好的解决方案。如果您不希望一直最大化所有内核,那么我建议将其保留为默认值,因为超线程在其他场景中肯定有一些性能优势。

但是,这完全取决于您的用例,唯一确定的方法是测试您的特定场景:

  • 磁盘 I/O 绑定?单个流程很可能是您的最佳选择。
  • CPU 受限?物理 CPU 内核的数量是您的最佳选择。
  • 网络 I/O 绑定?从默认值开始,并根据需要进行调整。
  • 没有明显的界限,但需要许多并行进程?也许你应该试试asyncio而不是multiprocessing

请注意,创建多个进程在内存和打开的文件方面并不是空闲的,而您可以拥有几乎无限量的协同路由—这不是进程的情况。根据您的操作系统配置,它可能会在您达到 100 之前达到最大值,即使您达到这些数字,CPU 调度将成为您的瓶颈。

创建一个员工库

创建工作进程的处理池通常是一项困难的任务。您需要处理调度作业、处理队列、处理进程,以及最困难的部分,即处理进程之间的同步,而不需要太多开销。

然而,有了multiprocessing这些问题已经解决了。您只需创建一个具有给定数量进程的进程池,并在需要时向其中添加任务即可。以下是 map 运算符的多进程版本示例,并演示了处理不会暂停应用:

import time
import multiprocessing

def busy_wait(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    n = 10000000
    items = [n for _ in range(8)]
    with multiprocessing.Pool() as pool:
        results = []
        start = time.time()
        print('Start processing...')
        for _ in range(5):
            results.append(pool.map_async(busy_wait, items))
        print('Still processing %.3f' % (time.time() - start))
        for result in results:
            result.wait()
            print('Result done %.3f' % (time.time() - start))
        print('Done processing: %.3f' % (time.time() - start))

处理本身非常简单。关键是池保持可用,您无需等待。只要在需要时添加作业,并在异步结果可用时立即使用它们:

# python3 test_pool.py
Start processing...
Still processing 0.000
Result done 1.513
Result done 2.984
Result done 4.463
Result done 5.978
Result done 7.388
Done processing: 7.388

进程间共享数据

这确实是多进程、多线程和分布式编程中最困难的部分—传递哪些数据,跳过哪些数据。然而,理论非常简单:只要有可能,就不要传输任何数据,不要共享任何内容,并将所有内容保持在本地。本质上是函数式编程范式,这就是为什么函数式编程与多进程很好地结合在一起。遗憾的是,在实践中,这并不总是可能的。multiprocessing库有几个共享数据的选项:PipeNamespaceQueue和其他一些选项。所有这些选项都可能诱使您始终在进程之间共享数据。这确实是可能的,但在许多情况下,性能影响比分布式计算提供的额外功率更大。所有数据共享选项都以所有处理内核之间的同步为代价,这需要花费大量时间。特别是对于分布式选项,这些同步可能需要几毫秒,如果全局执行,则会导致数百毫秒的延迟。

多进程命名空间的行为与常规对象的工作方式相同,但有一个小小的区别,即所有操作对于多进程都是安全的。有了所有这些功能,名称空间仍然非常易于使用:

import multiprocessing
manager = multiprocessing.Manager()
namespace = manager.Namespace()
namespace.spam = 123
namespace.eggs = 456

烟斗也没那么有趣。它只是一个双向通信端点,允许读写。在这方面,它只为您提供了一个读者和一个作者,因此,您可以组合多个进程/端点。同步数据时,您必须始终记住的唯一一件事是锁定需要时间。要设置正确的锁定,各方需要同意数据已锁定,这是一个需要时间的过程。而这一简单的事实大大降低了执行速度,超出了大多数人的预期。

在常规硬盘设置中,由于锁定和磁盘延迟,数据库服务器每秒无法在同一行上处理超过 10 个事务。使用延迟文件同步、SSD 和电池备份 RAID 缓存,性能可以提高到每秒处理同一行上的 100 个事务。这些都是简单的硬件限制,因为您有多个进程试图写入单个目标,您需要同步进程之间的操作,这需要很多时间。

“数据库服务器”统计信息是所有提供安全一致数据存储的数据库服务器的通用统计信息。

即使有最快的硬件可用,同步也会锁定所有进程并产生巨大的减速,因此如果可能,尽量避免在多个进程之间共享数据。简单地说,如果所有进程都从同一个对象读写,那么使用单个进程通常会更快。

远程进程

到目前为止,我们只在多个本地处理器上执行了脚本,但实际上我们可以进一步扩展它。使用multiprocessing库,在远程服务器上执行作业实际上非常容易,但文档目前仍然有点晦涩。实际上,有几种方法可以以分布式方式执行流程,但最明显的方法并不是最简单的。multiprocessing.connection模块同时具有ClientListener类,以简单的方式促进客户端和服务器之间的安全通信。通信与流程管理和队列管理不同,但是,这些功能需要一些额外的工作。在这方面,多进程库仍然有点空,但如果有几个不同的进程,它肯定是可能的。

使用多进程的分布式处理

首先,我们将从一个包含一些常量的模块开始,这些常量应该在所有客户端和服务器之间共享,因此服务器的机密密码和主机名对所有客户端和服务器都是可用的。除此之外,我们将添加我们的主要计算函数,稍后我们将使用这些函数。以下模块中的导入将预期此文件存储为constants.py,,但只要修改导入和引用,您可以随意调用它:

host = 'localhost'
port = 12345
password = b'some secret password'

def primes(n):
    for i, prime in enumerate(prime_generator()):
        if i == n:
            return prime

def prime_generator():
    n = 2
    primes = set()
    while True:
        for p in primes:
            if n % p == 0:
                break
        else:
            primes.add(n)
            yield n
        n += 1

现在是创建链接函数和作业队列的实际服务器的时候了:

import constants
import multiprocessing
from multiprocessing import managers

queue = multiprocessing.Queue()
manager = managers.BaseManager(address=('', constants.port),
                               authkey=constants.password)

manager.register('queue', callable=lambda: queue)
manager.register('primes', callable=constants.primes)

server = manager.get_server()
server.serve_forever()

在创建服务器之后,我们需要一个脚本来发送作业,它实际上是一个常规客户端。它非常简单,一个普通的客户机也可以作为处理器,但是为了让事情变得合理,我们将使用它们作为单独的脚本。以下脚本将向队列添加 0 到 999 以进行处理:

from multiprocessing import managers
import functions

manager = managers.BaseManager(
    address=(functions.host, functions.port),
    authkey=functions.password)
manager.register('queue')
manager.connect()

queue = manager.queue()
for i in range(1000):
    queue.put(i)

最后,我们需要创建一个客户端来实际处理队列:

from multiprocessing import managers
import functions

manager = managers.BaseManager(
    address=(functions.host, functions.port),
    authkey=functions.password)
manager.register('queue')
manager.register('primes')
manager.connect()

queue = manager.queue()
while not queue.empty():
    print(manager.primes(queue.get()))

从前面的代码中,您可以看到我们如何传递函数;管理器允许注册可以从客户端调用的函数和类。这样,我们就可以从多进程类传递一个队列,这个队列对于多线程和多进程都是安全的。现在我们需要启动流程本身。首先是持续运行的服务器:

# python3 multiprocessing_server.py

之后,运行 producer 生成主要生成请求:

# python3 multiprocessing_producer.py

现在我们可以在多台机器上运行多个客户机来获得前 1000 个素数。由于这些客户机现在打印前 1000 个素数,因此此处显示的输出有点太长,但您可以在多台机器上并行运行此操作以生成输出:

# python3 multiprocessing_client.py

如果愿意,您可以使用队列或管道将输出发送到不同的进程,而不是打印。正如您所看到的,并行处理仍然需要一些工作,并且需要一些代码同步才能工作。有几种替代品可用,如ØMQ芹菜异丙肾上腺素。其中哪一个是最好的和最合适的取决于您的用例。如果您只是在多个 CPU 上寻找处理任务,那么多进程和 IPyparallel 可能是您的最佳选择。如果您希望后台处理和/或轻松卸载到多台机器,那么ØMQ 和芹菜是更好的选择。

使用 IPyparallel 的分布式处理

IPyparallel 模块(以前称为 IPython Parallel)是一个非常容易同时在多台计算机上处理代码的模块。该库支持比您可能需要的功能更多的功能,但了解基本用法非常重要,以防您需要进行繁重的计算,这可能会从多台计算机中受益。首先,让我们从安装最新的 IPyparallel 软件包和所有 IPython 组件开始:

pip install -U ipython[all] ipyparallel

特别是在 Windows 上,使用 Anaconda 安装 IPython 可能更容易,因为它包含许多科学、数学、工程和数据分析软件包的二进制文件。为了获得一致的安装,Anaconda 安装程序也可用于 OS X 和 Linux 系统。

其次,我们需要集群配置。从技术上讲,这是可选的,但由于我们将创建一个分布式 IPython 群集,因此使用特定的配置文件配置所有内容要方便得多:

# ipython profile create --parallel --profile=mastering_python
[ProfileCreate] Generating default config file: '~/.ipython/profile_mastering_python/ipython_config.py'
[ProfileCreate] Generating default config file: '~/.ipython/profile_mastering_python/ipython_kernel_config.py'
[ProfileCreate] Generating default config file: '~/.ipython/profile_mastering_python/ipcontroller_config.py'
[ProfileCreate] Generating default config file: '~/.ipython/profile_mastering_python/ipengine_config.py'
[ProfileCreate] Generating default config file: '~/.ipython/profile_mastering_python/ipcluster_config.py'

这些配置文件包含大量选项,因此我建议搜索特定部分,而不是遍历它们。一个快速列表给出了这五个文件总共 2500 行的配置。文件名已经提供了有关配置文件用途的提示,但我们将更详细地解释它们,因为它们仍然有点混乱。

ipython_config.py

这是通用 IPython 配置文件;您可以在这里定制 IPython 外壳的几乎所有内容。它定义了 shell 的外观,默认情况下应该加载哪些模块,是否加载 GUI,等等。就本章而言,这并不是很重要,但如果您打算更频繁地使用 IPython,这绝对值得一看。您可以在这里配置的一个功能是自动加载扩展,如前一章中讨论的line_profilermemory_profiler。例如:

c.InteractiveShellApp.extensions = [
    'line_profiler',
    'memory_profiler',
]

ipython_kernel_config.py

此文件配置您的 IPython 内核,允许您覆盖/扩展ipython_config.py。为了理解它的用途,了解什么是 IPython 内核很重要。在此上下文中,内核是运行和内省代码的程序。默认情况下,这是IPyKernel,这是一个常规的 Python 解释器,但也有其他选项,如IRubyIJavascript分别运行 Ruby 或 JavaScript。

更有用的选项之一是可以为内核配置侦听端口和 IP 地址。默认情况下,端口都设置为使用随机数,但需要注意的是,如果其他人在您运行内核时可以访问同一台机器,他们将能够连接到您的 IPython 内核,这在共享机器上可能会很危险。

ipcontroller\u config.py

ipcontroller是您的 IPython 集群的主进程。它控制引擎和任务的分布,并负责日志记录等任务。

性能方面最重要的参数是TaskScheduler设置。默认情况下,c.TaskScheduler.scheme_name设置被设置为使用 Python LRU 调度程序,但根据您的工作负载,其他如leastloadweighted可能会更好。如果您必须在如此大的集群上处理如此多的任务,以至于调度器成为瓶颈,那么如果您的所有机器都具有相似的规格,并且任务具有相似的持续时间,那么plainrandom调度器也会出人意料地工作得很好。

出于测试目的,我们将控制器的 IP 设置为*,这意味着将接受所有IP 地址,并且将接受每个网络连接。如果您处于不安全的环境/网络和/或没有任何防火墙允许您有选择地启用某些 IP 地址,则不推荐使用此方法!在这种情况下,我建议通过更安全的选项来启动,比如SSHEngineSetLauncherWindowsHPCEngineSetLauncher

但是,假设您的网络确实安全,请将出厂 IP 设置为所有本地地址:

c.HubFactory.client_ip = '*'
c.RegistrationFactory.ip = '*'

现在启动控制器:

# ipcontroller --profile=mastering_python
[IPControllerApp] Hub listening on tcp://*:58412 for registration.
[IPControllerApp] Hub listening on tcp://127.0.0.1:58412 for registration.
[IPControllerApp] Hub using DB backend: 'NoDB'
[IPControllerApp] hub::created hub
[IPControllerApp] writing connection info to ~/.ipython/profile_mastering_python/security/ipcontroller-client.json
[IPControllerApp] writing connection info to ~/.ipython/profile_mastering_python/security/ipcontroller-engine.json
[IPControllerApp] task::using Python leastload Task scheduler
[IPControllerApp] Heartmonitor started
[IPControllerApp] Creating pid file: .ipython/profile_mastering_python/pid/ipcontroller.pid
[scheduler] Scheduler started [leastload]
[IPControllerApp] client::client b'\x00\x80\x00A\xa7' requested 'connection_request'
[IPControllerApp] client::client [b'\x00\x80\x00A\xa7'] connected

请注意写入概要文件目录的安全目录的文件。他们有ipengine用来查找ipcontroller的身份验证信息。它包含端口、加密密钥和 IP 地址。

iEngine_config.py

ipengine是实际的工人流程。这些过程运行实际的计算,因此为了加速处理,您需要在尽可能多的机器上使用这些过程。您可能不需要更改此文件,但如果要配置集中式日志记录或需要更改工作目录,则此文件可能非常有用。通常,您不希望手动启动ipengine进程,因为您很可能希望在每台计算机上启动多个进程。这就是我们的下一个命令,ipcluster命令。

ipcluster_config.py

ipcluster命令实际上只是一个简单的速记,可以同时启动ipcontrolleripengine的组合。对于一个简单的本地处理集群,我建议使用它,但在启动分布式集群时,可以使用ipcontrolleripengine的单独使用提供的控制。在大多数情况下,该命令提供了足够的选项,因此您可能不需要单独的命令。

最重要的配置选项是c.IPClusterEngines.engine_launcher_class,因为它控制发动机和控制器之间的通信方法。除此之外,它也是进程之间安全通信的最重要组件。默认情况下,它被设置为ipyparallel.apps.launcher.LocalControllerLauncher,这是为本地进程设计的,但如果您想使用 SSH 与客户机通信,ipyparallel.apps.launcher.SSHEngineSetLauncher也是一个选项。或适用于 Windows HPC 的ipyparallel.apps.launcher.WindowsHPCEngineSetLauncher

在所有机器上创建集群之前,我们需要传输配置文件。您可以选择传输所有文件,或者只传输 IPython 配置文件的security目录中的文件。

现在是启动集群的时候了,因为我们已经分别启动了ipcontroller,我们只需要启动引擎。在本地机器上,我们只需要启动它,但是其他机器还没有配置。一个选项是复制整个 IPython 概要文件目录,但真正需要复制的唯一文件是security/ipcontroller-engine.json。在使用配置文件创建命令创建配置文件后。因此,除非要复制整个 IPython 概要文件目录,否则需要再次执行概要文件创建命令:

# ipython profile create --parallel --profile=mastering_python

之后,只需复制ipcontroller-engine.json文件,即可完成。现在我们可以启动实际发动机:

# ipcluster engines --profile=mastering_python -n 4
[IPClusterEngines] IPython cluster: started
[IPClusterEngines] Starting engines with [daemon=False]
[IPClusterEngines] Starting 4 Engines with LocalEngineSetLauncher

注意这里的4是为四核处理器选择的,但任何数字都可以。默认的将使用逻辑处理器内核的数量,但根据工作负载的不同,最好匹配物理处理器内核的数量。

现在我们可以从 ipythonshell 运行一些并行代码。为了演示性能差异,我们将使用从 0 到 10000000 的所有数字的简单总和。这不是一项非常繁重的任务,但如果连续执行 10 次,常规 Python 解释器需要一段时间:

In [1]: %timeit for _ in range(10): sum(range(10000000))
1 loops, best of 3: 2.27 s per loop

然而,这次为了说明差异,我们将运行 100 次来演示分布式集群的速度。请注意,这是一个只有三台机器的集群,但仍然要快很多:

In [1]: import ipyparallel

In [2]: client = ipyparallel.Client(profile='mastering_python')

In [3]: view = client.load_balanced_view()

In [4]: %timeit view.map(lambda _: sum(range(10000000)), range(100)).wait()
1 loop, best of 3: 909 ms per loop

然而,更有趣的是 IPyParallel 中并行函数的定义。仅使用一个简单的装饰器,函数就被标记为并行:

In [1]: import ipyparallel

In [2]: client = ipyparallel.Client(profile='mastering_python')

In [3]: view = client.load_balanced_view()

In [4]: @view.parallel()
   ...: def loop():
   ...:     return sum(range(10000000))
   ...:

In [5]: loop.map(range(10))
Out[5]: <AsyncMapResult: loop>

IPyParallel 库提供了许多更有用的功能,但这超出了本书的范围。尽管 IPyParallel 是一个独立于 Jupyter/IPython 其余部分的实体,但它确实集成得很好,这使得组合它们变得非常容易。

使用 IPyParallel 最方便的方法之一是通过 Jupyter/IPython 笔记本电脑使用。为了演示,我们首先必须确保在 Jupyter 笔记本中启用并行处理,因为 IPython 笔记本默认执行单线程:

ipcluster nbextension enable

然后我们就可以开始notebook了,看看是怎么回事:

# jupyter notebook
Unrecognized JSON config file version, assuming version 1
Loading IPython parallel extension
Serving notebooks from local directory: ./
0 active kernels
The Jupyter Notebook is running at: http://localhost:8888/
Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).

使用 Jupyter 笔记本,您可以在 web 浏览器中创建脚本,以后可以轻松与他人共享。它对于共享脚本和调试代码非常有用,特别是因为网页(与命令行环境相反)可以轻松显示图像。这对绘制数据有很大帮助。以下是我们笔记本的屏幕截图:

ipcluster_config.py

总结

本章向我们展示了多进程是如何工作的,我们如何汇集大量作业,以及我们应该如何在多个进程之间共享数据。但更有趣的是,它还展示了我们如何在多台机器之间分配处理,这对加快繁重的计算有很大帮助。

从本章中您可以学到的最重要的一课是,您应该始终避免在多个进程或服务器之间进行数据共享和同步,因为这样做很慢,因此会大大降低应用的速度。尽可能将计算和数据保存在本地。

在下一章中,我们将学习如何在 C/C++中创建扩展,以提高性能并允许对内存和其他硬件资源的低级访问。虽然 Python 通常会保护你避免愚蠢的错误,但是 C 和 C++绝对不会。

|   | “C 很容易在脚上射中自己;C++使它变得更困难,但是当你这样做时,它会把你的整个腿吹走。” |   | |   | --比亚恩·斯特劳斯图普(C++的创建者) |