进阶篇 · Advanced
lv.3 进阶
kp-032
多进程与并行计算
1. 一句话定义
multiprocessing / ProcessPoolExecutor 用独立解释器进程绕开 GIL 实现真并行——每个进程有自己的内存与 GIL,代价是启动开销与参数/返回值的序列化。
2. 为什么重要
CPU 密集任务(批量解析、图像处理、数学计算)在 Python 里唯一的常规并行通道。也是理解"Python 慢但有并行补丁"叙事的关键一环。
3. 前置知识
kp-029(GIL)、kp-030(Executor API——两边同构)。
4. 核心概念
python
from concurrent.futures import ProcessPoolExecutor
import math
def is_prime(n: int) -> bool:
if n < 2: return False
return all(n % i for i in range(2, int(math.isqrt(n)) + 1))
with ProcessPoolExecutor(max_workers=4) as pool:
hits = list(pool.map(is_prime, range(2, 10**6))) # 真并行- 进程 vs 线程:进程不共享内存(各自独立 GIL),通信只能靠 pickle 序列化传递。
- 开销结构:进程启动(fork/spawn)+ 每次提交的序列化成本——任务要大而少,不要小而多。
- 进程安全入口:macOS/Windows 默认 spawn 启动方式,子进程会重新 import 主模块——必须有
if __name__ == "__main__":守卫。
5. 原理与机制
text
提交任务 fn(args):
主进程 ──pickle(args)──► 队列 ──► Worker 进程 unpickle → 执行 fn
◄──pickle(result)── 队列 ◄──
适合:单任务计算量 ≫ 序列化开销(ms 级以上)
不适合:几十微秒的小函数(序列化吃光收益)→ 用 NumPy 向量化或批处理共享内存(进阶):multiprocessing.Value/Array、shared_memory,或 Manager(代理对象,更慢但灵活)——日常优先"消息传递"而非"共享内存"。
6. 关键事实(模型/图示)
text
决策:CPU 密集?
是 → ProcessPoolExecutor(任务够大)或 NumPy 向量化(kp-041)
否 → 线程池 / asyncio(kp-030/031)
核数:os.cpu_count();workers 默认即核数7. 直观类比
多线程是同一间办公室的多个工位(共享文件柜,要排队协调——GIL/锁);多进程是多个独立办公室(各查各的文件柜,互不干扰),但每次传递结果都要装进箱子快递(pickle)——快递费(序列化)高于活儿本身就不划算。
8. 实例与案例
python
if __name__ == "__main__": # spawn 安全守卫,必写
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
# 批量图片压缩:每个任务是"大块头",序列化占比小
for path, ok in zip(paths, pool.map(compress_one, paths)):
print(path, ok)9. 常见误区
- 忘写
if __name__ == "__main__"—— spawn 子进程重新 import 主模块 → 无限递归建进程(Windows/macOS 必炸)。 - 传不可 pickle 的对象(连接、锁、lambda 闭包)——
PicklingError;任务函数必须是模块顶层可导入的名字。 - 任务太小 —— 序列化开销 > 计算;把 N 个小任务合并成 1 个批次。
- 期望多进程共享变量 —— 各进程地址空间独立;
global改了也只在子进程里生效。需要共享结果就靠返回值/队列。
10. 自测题
- 为什么多进程能绕过 GIL?每个进程有几把 GIL?
pool.map(small_fn, huge_list)慢在哪?- spawn 与 fork 的差别?为什么 macOS 默认 spawn?
参考答案
- 进程有独立内存空间与各自的解释器实例,也就有各自的 GIL;互不争抢。
- 每个任务都经历 pickle 参数→进程间传输→unpickle→执行→pickle 结果的完整往返,小任务的序列化成本超过计算本身。
- fork 复制父进程内存(快但继承状态复杂、与线程库有兼容坑);spawn 全新启动解释器重新 import(安全干净、慢);macOS 从 3.8 起默认 spawn 正因为 fork 与系统框架的线程安全问题。
11. 与其他知识点的关系
12. 延伸阅读
- multiprocessing 官方文档(Programming guidelines 一节必读)