多进程实现PageRank的并发问题:收敛异常及性能劣于串行的原因
问题场景
实现PageRank算法时,并行版本不仅运行速度慢于串行版本,还无法在相同迭代次数内收敛,并发行为与串行存在差异。串行与并行的核心累加逻辑一致,但并行使用multiprocessing.Manager的共享字典和进程池实现。
问题原因分析
1. 进程池重复创建导致巨大开销
并行代码中每次迭代都重新创建Pool对象:
for i in range(100): # ... 其他代码 pool = Pool() # ... 任务执行 pool.close() pool.join()
进程的创建、销毁以及进程间的资源调度是高开销操作,每次迭代都重复这个流程会完全抵消并行带来的性能收益,甚至比串行更慢。串行版本没有这部分额外开销,自然速度更快。
2. 共享字典的累加操作存在竞态条件
update函数中对sharedDict的+=操作并非原子操作:
sharedDict[neighbor] += *positive float(0->1) sharedDict[node] += *positive float(0->1)
多个进程同时对同一个键执行累加时,会出现"读-改-写"的竞态:进程A和B同时读取同一个键的当前值,分别完成加法后写回,最终只有一个进程的更新会生效,另一个的更新会丢失。这会导致并行计算的结果与串行逻辑完全不一致,迭代收敛的基础被破坏,自然无法在相同次数内收敛。
3. 共享字典的IPC通信开销过大
manager.dict()是通过进程间通信(IPC)实现的共享结构,所有对它的读写操作都需要经过主进程中转。相比于串行中直接操作本地字典,并行版本的每一次累加都要额外付出IPC通信的代价,当节点数量较多时,这部分开销会非常显著,进一步拖慢并行代码的运行速度。
4. 迭代初始值的传递逻辑隐含风险
并行代码中lastParallel = parallelDict是直接的引用赋值,虽然在当前代码逻辑中因为后续parallelDict被重新赋值为新字典不会引发问题,但这种写法容易在后续修改时导致引用错误(比如误操作lastParallel影响当前迭代)。串行版本同样存在这个问题,但并行场景下引用错误的影响会更隐蔽。
修复建议
- 将进程池的创建移到迭代循环外部,复用已创建的进程,避免重复初始化开销。
- 使用原子操作保护共享字典的累加,比如
multiprocessing.Lock,或者改用multiprocessing.Array等更高效的共享结构存储数值,减少竞态条件。 - 尽量减少共享数据的读写次数,比如让每个进程先在本地计算部分结果,最后再合并到共享结构中,降低IPC通信的频率。
内容的提问来源于stack exchange,提问作者BaccaRuler MC

