使用multiprocessing并行时全局列表增长导致性能骤降如何解决
多进程嵌套循环耗时随迭代急剧升高的解决方案
问题根源
耗时升高的核心原因有三类:
- 默认fork启动模式的复制开销:Unix/Linux下multiprocessing默认使用fork模式创建子进程,该模式会复制父进程的全部地址空间,随着
global_res不断增大,每次fork的耗时会线性升高。即使是写时复制机制,Python的列表这类包含大量指针的复合对象,子进程只要访问任意Python对象就很容易触发页复制,带来额外开销。 - 重复创建进程的冗余开销:示例代码每次内层循环都创建1000个Process实例,进程创建、销毁本身就是重开销操作,远大于示例中生成随机数组的计算开销。
- 跨进程共享对象的通信开销:
mp.Manager.list是跨进程代理对象,读写都需要序列化、进程间通信、反序列化,1000次读写的累积开销很高。
解决方案
1. 更换进程启动模式为spawn(解决全局变量不必要复制的核心方案)
spawn模式创建子进程时只会复制运行目标函数必须的资源,不会把父进程的所有全局变量全部复制到子进程空间,直接从源头避免大的global_res被复制的问题。使用方法非常简单,在代码入口添加启动模式设置即可:
import multiprocessing as mp if __name__ == '__main__': mp.set_start_method('spawn') # 后续业务代码都放在这个判断块内
注意:spawn模式要求所有多进程相关的执行逻辑必须放在
if __name__ == '__main__'的保护块内,否则会出现进程无限重复启动的问题。
2. 改用进程池复用进程,避免重复创建开销
不需要每次迭代都创建新进程,提前初始化和CPU核心数匹配的进程池,所有迭代复用固定数量的工作进程,可大幅降低进程管理开销。
3. 替换Manager共享对象为进程池原生返回值传递
进程池的map/apply_async自带结果返回机制,不需要额外用Manager的共享列表传递结果,可大幅降低跨进程通信的开销。
优化后的完整代码
import time import numpy as np import multiprocessing as mp # 工作函数放到顶层作用域,spawn模式需要能导入该函数 def process_with_big_output(i): aux = np.random.randint(0, 10, size=100000) return aux if __name__ == '__main__': # 核心:设置spawn启动模式,避免不必要的全局变量复制 mp.set_start_method('spawn') repeats = 100 elapsed = [] only_parallel = [] global_res = [None for _ in range(repeats)] N = 1000 # 提前初始化进程池,进程数和CPU核心数匹配即可,无需和任务数一致 with mp.Pool() as pool: for itr in range(repeats): start = time.time() # 直接用map批量提交任务,自动回收结果 all_res = pool.map(process_with_big_output, range(N)) only_parallel.append(time.time() - start) global_res[itr] = all_res elapsed.append(time.time() - start) if itr % 10 == 0: print(only_parallel[-1], elapsed[-1])
其他可选方案(如果必须使用fork模式)
如果场景必须使用fork启动模式,可以把不需要传递给子进程的大对象从顶层全局作用域移到if __name__ == '__main__'的块内,或者用函数封装逻辑,不要让大对象出现在工作函数能访问到的全局作用域中,就可以避免fork时被复制。
内容的提问来源于stack exchange,提问作者Sasha Sologub
相关产品推荐
相关产品推荐

