Python多线程无法加速CPU密集型任务,因CPython的GIL限制;应改用ProcessPoolExecutor,注意函数需顶层定义、参数可序列化、Windows/macOS须加if name == "__main__":保护。

怎么判断你真需要multiprocessing而不是threading
别凭感觉,看实际行为:psutil.cpu_percent(percpu=True)返回的数组里,是否只有1个核长期接近100%,其余核低于5%?任务耗时是否随max_workers增加不降反升?如果是,基本锁定为CPU密集型场景。常见误判点:把“有网络请求”当成IO密集型——如果请求后立刻做大量JSON解析、正则匹配或数据聚合,计算部分已压过IO等待,GIL就又锁死了。真正纯IO密集(如大批量HTTP GET、文件流读取)才适合ThreadPoolExecutor。
ProcessPoolExecutor比multiprocessing.Pool更推荐用
两者底层都走multiprocessing,但ProcessPoolExecutor接口统一、异常传播清晰、资源自动清理,和ThreadPoolExecutor写法几乎一样,切换成本极低。关键差异点:
-
Pool.map()默认阻塞,ProcessPoolExecutor.map()返回迭代器,需list()或循环消费才触发执行 -
Pool的initializer参数可预加载模块/大对象,ProcessPoolExecutor没直接等价物,得靠全局变量+if __name__ == "__main__":保护 - Windows/macOS下
Pool默认fork启动易出错,ProcessPoolExecutor可通过mp.set_start_method('spawn')提前指定
函数和参数必须能被pickle序列化
这是最常踩的坑:嵌套函数、lambda、类实例方法、带闭包的函数,全会报PicklingError。解决路径很明确:
- 把目标函数定义在模块顶层,不要缩进在另一个函数里
- 避免用
self.xxx,改用普通函数+显式传参(如def process_item(data, config)) - 大数组或DataFrame别直接当参数传——序列化开销大且可能失败;改用
multiprocessing.shared_memory或提前存到磁盘,子进程只传路径或共享名 - 如果必须用类逻辑,定义成可调用对象:
class Worker: def __call__(self, x): return x**2,然后executor.map(Worker(), data_list)
Windows/macOS下必加if __name__ == "__main__":保护
不加这句,子进程会重新导入主模块,导致递归创建新进程池,最终卡死或爆内存。这不是建议,是硬性要求。尤其注意:Jupyter Notebook里运行时,__name__永远是'__main__',但实际执行环境仍是模块上下文,所以仍需包裹——简单起见,所有含ProcessPoolExecutor或Pool的脚本,开头就写这行,别省。
立即学习“Python免费学习笔记(深入)”;
最易忽略的是:进程间无共享内存,所有通信都要显式序列化。你以为传了个字典进去,其实背后是pickle.dumps + pipe传输 + pickle.loads三连开销。计算越轻、数据越大,这个开销占比越高——这时候反而单进程+NumPy向量化更快。


















