1. 课程导入与数值任务并行场景
在 Python 里做数值计算,很多人第一反应是“用 NumPy 就够了”。NumPy 的向量化确实能把逐元素循环压进 C 层,但一旦任务本身是大量相互独立的重复计算,或者单次计算耗时很长、内存占用很大,单进程就会成为瓶颈。这时真正需要的不是更快的单次运算,而是让多个 CPU 核心同时干活——这正是多进程并行要解决的问题。
先厘清一个关键前提:Python 存在全局解释器锁(GIL),同一进程内多个线程无法同时执行 Python 字节码。对于纯 Python 的数值循环,多线程往往无法提速;而 multiprocessing 会创建独立的子进程,每个进程有自己的解释器和内存空间,从而真正利用多核。代价是进程间通信需要序列化,数据不能像线程那样直接共享。理解“独立进程 + 序列化传递”这一模型,是后续所有内容的基础。
多进程并行在数值计算中的价值
数值任务并行带来的收益可以用一个简单关系判断:设单任务耗时 T,任务数 N,可用核心数 P,则理想加速比接近 min(N, P)。但实际加速会被三部分吃掉:进程启动开销、任务数据的序列化与传输、以及结果汇总。因此多进程最适合的场景具有共同特征——任务之间几乎不依赖、单个任务计算量足够大、输入输出数据量相对可控。如果每个任务只有几毫秒,进程调度和序列化的开销可能超过计算本身,此时并行反而更慢。
下面这张图概括了本节要建立的整体认知:从串行数值任务出发,经过任务切分、进程分发、并行计算,再到结果汇总,同时标出了影响加速效果的关键因素。
三个典型应用场景
蒙特卡洛模拟是最典型的“天然并行”任务。例如用随机投点法估计圆周率:在单位正方形内撒 N 个点,统计落入四分之一圆内的比例,乘以 4 即为 π 的估计。每个样本点相互独立,把 N 拆成 P 份交给不同进程,各自统计命中数,最后求和即可。这里的关键是每个子进程使用不同的随机种子,否则会得到重复样本,导致结果偏差。
矩阵运算适合按块并行。比如计算两个大矩阵的乘积,可以把结果矩阵按行分块,每个进程负责若干行与整个右矩阵相乘。需要注意,如果直接把大矩阵作为参数传给每个子进程,pickle 序列化的开销会非常可观;更合理的做法是让子进程通过共享内存或内存映射文件读取数据,只传递索引和形状信息。
参数扫描常见于模型调参与敏感性分析。假设要在一组超参数网格上评估模型,每个参数组合的训练与评估互不依赖,正好可以映射为进程池中的一个个任务。这类场景的难点在于负载均衡:不同参数组合的耗时可能差异很大,需要用动态调度而非静态均分。
课程学习路径与实战项目
本课程按“先建立模型、再掌握工具、最后落地项目”的顺序展开。前期会讲清进程、进程池、任务分发与结果收集的基本机制,以及 multiprocessing 中 Pool、Process、Queue、Manager 等核心对象的使用边界;中期进入数值场景的专门技巧,包括随机数种子的正确设置、大数组的共享内存方案、分块策略与负载均衡、异常在子进程中的传播与处理;后期通过一个贯穿式实战项目把上述知识串起来。
实战项目设定为“蒙特卡洛估算 π + 参数扫描调优”的组合任务:先实现串行版本并测量基线耗时,再用进程池并行化,对比不同进程数下的加速比,观察序列化开销与负载不均衡带来的影响,最后给出在给定机器上选择进程数的判断依据。项目不追求复杂,但要求每一步都有可观察的输入、可测量的中间结果和明确的判断标准,避免“跑通即结束”。
time.perf_counter() 记录耗时。只有对比数据,才能判断并行是否真的带来了收益,以及瓶颈出在计算、传输还是调度上。