我阅读了文档,试图获得基本理解,但它仅仅表明 ProcessPoolExecutor
允许绕过全局解释器锁 Global Interpreter Lock
,我认为这是对变量或函数进行锁定的方式,从而使并行进程不会同时更新其值。
我想知道何时使用 ProcessPoolExecutor
和何时使用 ThreadPoolExecutor
,以及在使用每种方法时需要注意什么!
我阅读了文档,试图获得基本理解,但它仅仅表明 ProcessPoolExecutor
允许绕过全局解释器锁 Global Interpreter Lock
,我认为这是对变量或函数进行锁定的方式,从而使并行进程不会同时更新其值。
我想知道何时使用 ProcessPoolExecutor
和何时使用 ThreadPoolExecutor
,以及在使用每种方法时需要注意什么!
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
模块中更加复杂的显式共享数据包装。
ProcessPool 用于处理 CPU 绑定的任务,因此您可以从多个CPU获益。
Threads 用于处理 I/O 绑定的任务,因此您可以从 I/O 等待中获益。