ProcessPoolExecutor和ThreadPoolExecutor有什么区别?

61

我阅读了文档,试图获得基本理解,但它仅仅表明 ProcessPoolExecutor 允许绕过全局解释器锁 Global Interpreter Lock,我认为这是对变量或函数进行锁定的方式,从而使并行进程不会同时更新其值。

我想知道何时使用 ProcessPoolExecutor 和何时使用 ThreadPoolExecutor,以及在使用每种方法时需要注意什么!

2个回答

91

ProcessPoolExecutor 在独立的子进程中运行每个工作线程。

ThreadPoolExecutor 在主进程内的单独线程中运行每个工作线程。

全局解释器锁(GIL)不仅锁定变量或函数,而是锁定整个解释器。这意味着每个内置操作,包括像listodicts[3]['spam'] = eggs这样的操作,都自动具有线程安全性。

但是这也意味着,如果您的代码受到CPU限制(即,它花费时间进行计算,而不是等待网络响应等),并且没有在设计释放GIL的外部库中花费大部分时间(如NumPy),则同一时间只能有一个线程拥有GIL。因此,即使您有4个线程,即使您有4个或甚至16个核心,大多数情况下,其中3个将坐在那里等待GIL。因此,您的代码并没有获得4倍的加速,反而会变慢一些。

同样,对于I/O绑定的代码(例如,等待一堆服务器响应您发出的一堆HTTP请求),线程就很好用; 只有对于CPU绑定的代码才会出现这个问题。

每个独立的子进程都有自己独立的GIL,因此这个问题就解决了——即使您的代码受到CPU限制,使用4个子进程仍然可以使其运行速度快近4倍。

但是子进程不共享任何变量。通常情况下,这是件好事——你将值(的副本)作为参数传递给函数,并返回(的副本)值,进程隔离保证了安全性。但是偶尔情况下(通常是出于性能原因,但有时也是因为你正在传递无法通过pickle复制的对象),这是不可接受的,所以你需要使用线程或者使用multiprocessing模块中更加复杂的显式共享数据包装。


关于线程,为什么它对于大量HTTP请求很有用?例如,我有3个请求和3个线程,每次CPU轮询其中一个线程并在获取数据方面做出一些小进展,这比依次从每个请求中获取所有数据要好吗?我可以重新表述问题,当一个休眠的线程发送Web请求时,即使CPU运行另一个线程,他也会得到答案吗?如果是这样,那么这是如何可能的? - TheLogicGuy

52

ProcessPool 用于处理 CPU 绑定的任务,因此您可以从多个CPU获益。

Threads 用于处理 I/O 绑定的任务,因此您可以从 I/O 等待中获益。


4
我真的很喜欢这个答案的简洁明了。虽然这个答案本身不足以满足我的需求(因为它没有解释原因,所以我不能只凭它就运行代码),但是它通过使用简单易懂的语言和少量术语,帮助我理解了正式答案。 - Slartibartfast
这是一个经验法则,但有点简化了。如果您有许多短暂的CPU绑定任务,则由于创建和管理多个进程的开销,使用线程可能实际上更好,因为与实际执行任务所花费的时间相比,创建进程的成本可能相当大。 - hwaxxer

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