>>>PyPathPython 学习站
首页›进阶篇›kp-032
进阶篇 · Advanced lv.3 进阶 kp-032

多进程与并行计算

前置知识:kp-029、kp-030

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. 常见误区

  1. 忘写 if __name__ == "__main__" —— spawn 子进程重新 import 主模块 → 无限递归建进程(Windows/macOS 必炸)。
  2. 传不可 pickle 的对象(连接、锁、lambda 闭包)—— PicklingError;任务函数必须是模块顶层可导入的名字。
  3. 任务太小 —— 序列化开销 > 计算;把 N 个小任务合并成 1 个批次。
  4. 期望多进程共享变量 —— 各进程地址空间独立;global 改了也只在子进程里生效。需要共享结果就靠返回值/队列。

10. 自测题

  1. 为什么多进程能绕过 GIL?每个进程有几把 GIL?
  2. pool.map(small_fn, huge_list) 慢在哪?
  3. spawn 与 fork 的差别?为什么 macOS 默认 spawn?
参考答案
  1. 进程有独立内存空间与各自的解释器实例,也就有各自的 GIL;互不争抢。
  2. 每个任务都经历 pickle 参数→进程间传输→unpickle→执行→pickle 结果的完整往返,小任务的序列化成本超过计算本身。
  3. fork 复制父进程内存(快但继承状态复杂、与线程库有兼容坑);spawn 全新启动解释器重新 import(安全干净、慢);macOS 从 3.8 起默认 spawn 正因为 fork 与系统框架的线程安全问题。

11. 与其他知识点的关系

  • kp-041 数据科学生态:NumPy/Polars 在 C 层并行,多数场景替代手写多进程。
  • kp-034 性能:先测再并——并行不能救活坏算法。
  • kp-029 GIL:本篇是它的"官方逃生舱"。

12. 延伸阅读

  • multiprocessing 官方文档(Programming guidelines 一节必读)