Python CFD代码多进程/多线程并行时崩溃及结果异常问题问询
问题原因
1. 早期细粒度任务崩溃原因
每个三维网格点单独创建进程时,单次迭代需要创建的进程数等于网格点总数,进程本身的内存开销、调度开销远大于计算开销,会快速耗尽系统内存和进程资源,最终导致系统冻结崩溃。改用i维度拆分的粗粒度任务后,单次迭代的进程数只有N+2个,开销大幅降低,因此不再崩溃。
2. 多进程计算结果为初始值的原因
Python multiprocessing 模块创建的子进程默认拥有独立的虚拟地址空间,你代码中作为全局变量的温度数组T,在每个子进程中都是主进程的独立拷贝,子进程对T的所有修改只会作用于自身的副本,完全不会同步回主进程,因此主进程中的T永远停留在初始化数值。
而你之前用多线程能得到正确结果是因为同一进程下的所有线程共享地址空间,修改全局变量对所有线程可见,但Python全局解释器锁(GIL)会导致CPU密集型任务无法实现真正的多线程并行,因此运行速度远慢于串行。
解决方案
方案1:使用共享内存数组(性能最优)
直接创建所有进程共享的内存区域存储温度数组,无需额外数据拷贝,性能损失最小。由于你的任务是按i维度拆分,不同进程操作的数组索引完全不重叠,不存在写冲突,不需要加锁:
import multiprocessing as mp import numpy as np # 网格尺寸定义 N = 你的网格尺寸 array_size = (N + 2) ** 3 # 创建双精度类型的共享内存数组,关闭锁避免不必要开销 shared_T = mp.Array('d', array_size, lock=False) # 封装为numpy数组视图,直接操作共享内存 T = np.frombuffer(shared_T, dtype=np.float64) # 按照原有逻辑初始化T的数值 # ... 初始化代码省略 ... def iterative(i): # 原有计算逻辑完全不需要修改,直接操作共享内存的T数组即可 for j in range(0, N+2): for k in range(0, N+2): ind = j*(N+2)+i+k*((N+1)*(N+2)+N+2) # 原有边界条件、迭代计算逻辑完全保留 # ... 原有代码省略 ...
注意主进程调用时需要等待所有子进程计算完成再做后续操作,修改原有进程池调用逻辑:
for n in range(0,8): print(n) # 用map方法阻塞等待所有i对应的任务完成 pool2.map(iterative, range(0, N+2)) # 后续的存文件、绘图逻辑保持不变 # ... 原有代码省略 ...
方案2:子进程返回结果主进程合并(逻辑最简单)
如果不想处理共享内存,可修改iterative函数返回当前i对应的所有计算结果,主进程收集后合并到全局数组,适合网格尺寸不大的场景:
def iterative(i): # 临时存储当前i对应的所有j、k的计算结果 res = np.zeros((N+2, N+2), dtype=np.float64) for j in range(0, N+2): for k in range(0, N+2): ind = j*(N+2)+i+k*((N+1)*(N+2)+N+2) # 原有计算逻辑,将给T[ind]赋值改为给res[j,k]赋值 # ... 修改后的计算逻辑省略 ... return i, res # 主进程调用逻辑 for n in range(0,8): print(n) # 收集所有子进程的返回结果 results = pool2.map(iterative, range(0, N+2)) # 合并结果到主进程的T数组 for i, res in results: for j in range(0, N+2): for k in range(0, N+2): ind = j*(N+2)+i+k*((N+1)*(N+2)+N+2) T[ind] = res[j,k] # 后续存文件、绘图逻辑保持不变 # ... 原有代码省略 ...
额外优化建议
如果可以接受引入第三方库,使用numba的并行装饰器是成本最低的优化方案,不需要手动处理多进程逻辑,仅需要给迭代函数加@njit(parallel=True)装饰器,就能获得接近原生C的并行计算性能,非常适合CFD这类数值计算场景。
内容的提问来源于stack exchange,提问作者Jejouze

