上一篇 多线程 threading 基础 讲了多线程受 GIL 限制,不适合 CPU 密集任务。这一篇讲多进程——每个进程一个独立 Python 解释器,各有自己的 GIL,真正并行。适合视频处理、数据分析、机器学习推理等 CPU 密集场景。
一、进程 vs 线程
| 对比项 | 线程 | 进程 |
|---|---|---|
| 共享内存 | ✅ 天然共享 | ❌ 各自独立,需要 IPC |
| 创建开销 | 小(毫秒级) | 大(几十毫秒到几百毫秒) |
| 数据传递 | 直接引用 | 序列化后再发(pickle) |
| CPU 并行 | ❌ 受 GIL 限制 | ✅ 真正并行 |
| 崩溃影响 | 整个进程挂 | 只挂当前进程 |
规则:
- CPU 密集 → 多进程;
- IO 密集 → 多线程或异步。
二、创建进程
1 | from multiprocessing import Process |
关键:if __name__ == "__main__": 不能省——尤其在 Windows/macOS 上。子进程会重新 import 主脚本,如果没有这个保护,会陷入无限递归创建进程。
三、ProcessPoolExecutor:Pythonic 姿势
跟线程池 API 完全一样:
1 | from concurrent.futures import ProcessPoolExecutor |
max_workers 默认是 CPU 核心数。
四、Pool:老一代 API
multiprocessing.Pool 是更老的 API,功能类似:
1 | from multiprocessing import Pool |
新代码优先用 ProcessPoolExecutor——它跟 ThreadPoolExecutor 的接口统一,方便切换。
五、任务函数的要求
传给多进程的函数必须能被 pickle:
- 可以:模块顶层的普通函数、类的方法(类必须是顶层的);
- 不可以:lambda、内嵌函数、局部类的方法。
1 | # ❌ Pool 里报错 |
六、共享数据
进程间不共享内存,要传数据用:
队列 Queue(多进程版)
1 | from multiprocessing import Process, Queue |
Pipe:管道(双向通信)
1 | from multiprocessing import Pipe |
Manager:共享对象
1 | from multiprocessing import Manager |
Manager 比 Queue 慢——它把所有访问都变成 IPC 调用。只在需要”共享结构化状态”时用。
共享内存
Python 3.8+ 有更高性能的共享内存:
1 | from multiprocessing.shared_memory import SharedMemory |
一般项目用不到,做高性能数据分析时才需要。
七、数据序列化开销
多进程之间传数据要经过 pickle——这是最大的性能陷阱:
- 传大 numpy 数组:慢;
- 传函数(lambda):不能 pickle,报错;
- 传数据库连接、文件句柄:不能 pickle。
优化:
- 让子进程”自己去拿数据”(读文件而不是父进程传进来);
- 数据分片,一次传一小片;
- NumPy 数据用 shared_memory 或
Array/Value; - 少用 Manager 共享结构。
八、进程池 vs 手动 Process
- 进程池(Pool / ProcessPoolExecutor):推荐,自动管理生命周期;
- 手动 Process:控制精细,适合特殊场景(长期后台进程)。
九、启动方式:fork vs spawn vs forkserver
- fork(Linux/macOS 默认):复制父进程所有内存,快,但可能有资源泄漏;
- spawn(Windows 唯一,macOS 3.8+ 默认):从头启动新进程,慢但干净;
- forkserver:fork 一个”服务器进程”,其他都从它 fork。
1 | import multiprocessing as mp |
十、CPU 密集加速实测
1 | import time |
十一、初始化每个进程
worker 进程启动时想做初始化(连数据库、加载模型):
1 | def init_worker(): |
十二、任务超时
ProcessPoolExecutor 的 future.result(timeout=...) 会等超时——但不会真的杀掉进程,只是抛 TimeoutError。想强制杀 worker,改用 pool.shutdown(cancel_futures=True)(Python 3.9+),或者用 pebble 第三方库。
十三、joblib:更简洁的并行
数据分析场景推荐 joblib(scikit-learn 用的库):
1 | from joblib import Parallel, delayed |
n_jobs=-1 用所有核心。API 比 ProcessPoolExecutor 更紧凑。
十四、一个完整例子:批量图片处理
1 | # imgproc.py |
if __name__ == "__main__": 里调用是 Windows/macOS 上多进程的硬性要求。
十五、常见陷阱
- 忘
if __name__ == "__main__"::Windows 上会陷入无限创建子进程。 - 传 lambda 给 Pool:pickle 报错。
- 进程数远超 CPU 核心:调度开销大,反而变慢。
- 传大数据:序列化开销比计算还大。让子进程自己读磁盘。
- 进程池里用 print:多个进程混着输出,看起来乱。用 logging 会好些。
- 共享 Manager 对象锁死:Manager 内部有锁,大量并发写会锁竞争。改用 shared_memory。
- 忘 shutdown:进程可能滞留。用
with或显式pool.shutdown()。
十六、小结与延伸阅读
- 多进程绕开 GIL,真正并行;
- 优先用
ProcessPoolExecutor; - 函数必须能 pickle(顶层普通函数);
- 数据传递有序列化开销,尽量少传;
- 进程数 = CPU 核心数附近;
- 保护主入口
if __name__ == "__main__":; - CPU 密集用它,IO 密集用 threading/asyncio。
延伸阅读:
- multiprocessing:https://docs.python.org/zh-cn/3/library/multiprocessing.html
- concurrent.futures:https://docs.python.org/zh-cn/3/library/concurrent.futures.html
- joblib:https://joblib.readthedocs.io/
下一篇 异步编程 asyncio 入门 我们讲现代 Python 里最流行的并发方式——协程。