Python的multiprocessing和concurrent.futures有什么区别?

24

在Python中实现多进程的简单方法是

from multiprocessing import Pool

def calculate(number):
    return number

if __name__ == '__main__':
    pool = Pool()
    result = pool.map(calculate, range(4))

一个基于futures的替代实现是

from concurrent.futures import ProcessPoolExecutor

def calculate(number):
    return number

with ProcessPoolExecutor() as executor:
    result = executor.map(calculate, range(4))

两种替代方案基本上都是做相同的事情,但一个显著的区别是我们不必使用通常的if __name__ == '__main__'子句来保护代码。这是因为futures的实现照顾到了这一点,还是有其他原因?

更广泛地讲,multiprocessingconcurrent.futures之间有什么区别?什么时候会更喜欢其中之一?

编辑:我的最初假设是只有在多进程处理中才需要保护if __name__ == '__main__'。这是错误的。显然,在Windows系统上,两种实现都需要这个保护措施,而在Unix系统上则不需要。


3
怀疑不需要这个if保护语句。根据文档ProcessPoolExecutor是建立在multiprocessing之上的,因此它应该遇到同样的问题(否则multiprocessing文档将展示如何避免该保护语句,对吧?)。实际上,来自文档的示例确实使用了通常的保护语句。 - Bakuriu
你说得对。因为显然只有在Windows上才需要,所以我感到有些困惑。我必须承认我只在Mac上测试了这个功能,因此发现守卫不是必需的。我会在问题中添加一些注释来强调这一点。 - David Zwicker
有一次我因为忘记了保护装置而导致一台刀片服务器崩溃 :) - JamesHutchison
请参见https://dev59.com/T2Ii5IYBdhLWcg3w9wUU。 - max
看起来在Unix上使用prefork模型可以避免那个“if”行,有人能确认吗? - James
2个回答

29
你实际上应该使用 if __name__ == "__main__" 保护与 ProcessPoolExecutor 相关的内容,因为它在底层使用 multiprocessing.Process 来填充其 Pool,就像 multiprocessing.Pool 一样,所以所有与可拾取性相关的注意事项(特别是在Windows上)等都适用。
我相信,根据这篇文章(Python核心贡献者Jesse Noller发布的声明),ProcessPoolExecutor 意在最终取代multiprocessing.Pool
Brian 和我需要在人们熟悉这两个API之后进行整合工作。我的最终目标是将除了基本 multiprocessing.Process/Queue 之外的所有东西从MP中移除并转移到 concurrent.* ,并支持线程后端。

目前,ProcessPoolExecutor 大多数情况下与 multiprocessing.Pool 做了完全相同的事情,但 API 更简单(也更有限)。如果您可以使用 ProcessPoolExecutor,请使用它,因为我认为它更有可能在长期内得到增强。请注意,您可以使用所有来自 multiprocessing 的帮助程序与 ProcessPoolExecutor,如 LockQueueManager 等,因此需要这些不是使用 multiprocessing.Pool 的原因。

然而,它们的 API 和行为存在一些值得注意的差异:

  1. 如果一个进程在 ProcessPoolExecutor 中突然终止,会引发一个 BrokenProcessPool 异常,中止任何等待池完成工作的调用,并防止提交新的工作。如果同样的事情发生在 multiprocessing.Pool 中,它将默默地替换终止的进程,但是在该进程中正在进行的工作将永远不会完成,这可能会导致调用代码永远挂起等待工作完成。

  2. 如果您运行的是 Python 3.6 或更低版本,则无法使用 ProcessPoolExecutorinitializer/initargs 功能。只有在 3.7 中才添加了对此的支持

  3. ProcessPoolExecutor 不支持 maxtasksperchild

  4. 在 Python 2.7 中,除非手动安装回退程序,否则不存在 concurrent.futures

  5. 如果您运行的是 Python 3.5 以下版本,则根据 这个问题multiprocessing.Pool.map 的性能优于 ProcessPoolExecutor.map。请注意,每个工作项的性能差异非常小,因此只有在使用 map 处理非常大的可迭代对象时,才会注意到较大的性能差异。性能差异的原因在于,multiprocessing.Pool 将传递给 map 的可迭代对象分批处理成块,然后将这些块传递给工作进程,从而减少了父进程和子进程之间的 IPC 开销。ProcessPoolExecutor 总是(或默认情况下,在 3.5 中开始)一次从可迭代对象中传递一个项目给子进程,这可能会导致处理大型可迭代对象时性能较慢,因为增加了 IPC 开销。好消息是,Python 3.5 中已经解决了这个问题,因为在 ProcessPoolExecutor.map 中添加了一个 chunksize 关键字参数,可以用于在处理大型可迭代对象时指定更大的块大小。详情请参见 此 bug


从当前的 源代码 来看,使用 chunksize > 1 的 ProcessPoolExecutor.map 函数似乎会将元组发送到函数中,因此该函数需要能够处理项目的元组而不是单个项目。您认为我理解得正确吗? - wwii
1
@wwii 该函数返回的元组由 _process_chunk 方法处理,该方法将元组中的每个条目提取出来,并将其传递给用户提供的映射函数。因此,当用户使用 chunksize > 1 时,用户无需更改任何内容。 - dano
1
@Jay 不,这两个缺陷都已经得到解决。chunksize在3.5中被添加到map中,而initializer/initargs则在3.7中被添加。 - dano

3
如果 __name__ == '__main__',这意味着你是在命令提示符下使用 python [options] 调用脚本,而不是在 Python shell 中使用 import 。
当你从命令提示符调用脚本时,__main__ 方法会被调用。在第二个代码块中,

with ProcessPoolExecutor() as executor:
    result = executor.map(calculate, range(4))

无论是从命令提示符还是从shell导入,该程序块都会被执行。

2
实际上,在Windows上,需要保护multiprocessing脚本的__main__,因为主体在子进程中重新执行。 - Antti Haapala -- Слава Україні
啊,那样的话我误解了问题。 - user2867522

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接