process 启动比 thread 慢因需 fork/spawn 复制内存、重载模块;子进程重复初始化易致卡顿或 oom,应将耗时操作移至主进程或用 initializer 控制;windows/macos 默认 spawn 需重导入,linux fork 可能死锁;queue 线程安全但有锁开销,pipe 更快但仅双端;全局变量不共享,应用 value/array;processpoolexecutor 默认 cpu 核数对 i/o 任务过载,需调优 max_workers 并异步取结果;性能瓶颈常在 pickle 和 ipc,非单纯核数问题。

为什么 Process 启动比 Thread 慢得多
因为每次创建 Process 都要 fork(Unix/Linux/macOS)或 spawn(Windows),复制整个解释器内存空间,加载模块、重建对象图——这不是“开个线程”那种轻量操作。
常见错误现象:Process(target=heavy_init_func).start() 中 heavy_init_func 在子进程里重复执行初始化(如加载大模型、读大文件),导致启动卡顿甚至 OOM。
- 把耗时初始化移到
if __name__ == '__main__':块外,但确保只在主进程运行;子进程用if hasattr(sys, '_called_from_multiprocessing'):或更稳妥地:用initializer+initargs显式控制 - Windows/macOS 默认用
'spawn'启动方式,必须重新 import 所有依赖模块;Linux 默认'fork'虽快,但若主进程已调用threading.Lock或logging等非 fork-safe 对象,子进程可能死锁 - 用
mp.set_start_method('spawn', force=True)统一行为,尤其在打包成可执行文件(PyInstaller)时,'fork'会失效
Queue 和 Pipe 选哪个?吞吐差 3–5 倍
Queue 是线程安全的封装,底层用 Pipe + threading.Lock + 后台线程做收发;Pipe 是裸的双端通道,无锁,但只能连两个进程。
使用场景:高频小数据(如每秒万级日志条目)传参,Pipe 更稳;多对一聚合(如 8 个 worker 往一个 collector 发结果),必须用 Queue。
立即学习“Python免费学习笔记(深入)”;
-
Queue的put()默认阻塞,若消费者崩溃或没及时get(),队列填满后生产者永久卡住——加timeout并捕获queue.Full -
Pipe的recv()在另一端关闭后会抛EOFError,不是None;别用while conn.poll(): conn.recv()轮询,CPU 占满;改用conn.poll(timeout) - 传输大于 1MB 的对象时,
Queue和Pipe都会触发 pickle 序列化开销;考虑用mmap或shared_memory(Python 3.8+)传原始字节
全局变量在子进程里“不更新”的真相
每个 Process 有独立内存空间,主进程里的全局变量只是被 copy 了一份,改了等于没改。这不是 bug,是设计如此。
常见错误现象:定义 CONFIG = {'debug': True},主进程改 CONFIG['debug'] = False,子进程里打印还是 True。
- 想共享简单值(int/bool/float),用
mp.Value或mp.Array;注意类型声明必须精确,比如mp.Value('i', 0)不能存浮点数 - 想共享字典或列表,别用
mp.Manager().dict()—— 它走网络协议模拟,慢且单点瓶颈;真需要复杂结构,用concurrent.futures.ProcessPoolExecutor+ 显式传参,避免共享 - 用
@mp.context._ForkContext(非公开 API)强行绕过 fork/spawn 差异?别试。跨平台行为不可控,PyPI 包里已有人踩坑崩溃
为什么 ProcessPoolExecutor 有时比手写 Process 还慢
因为默认最大工作进程数 = os.cpu_count(),但如果你的任务是 I/O 密集型(比如发 HTTP 请求),开满 CPU 核反而导致频繁上下文切换和连接池争抢。
性能影响:CPU 密集任务(如计算 π、图像处理)适合 max_workers=os.cpu_count();I/O 密集任务(如爬虫、数据库查询)设为 4~8 更稳。
-
submit()返回Future,但很多人直接future.result()同步等,等于退化成串行——该用as_completed()或map()批量提交+异步取结果 - 子进程异常不会自动打印,
future.exception()是None直到你调用result()才抛出;加日志钩子:在initializer里配置logging.basicConfig(),否则 stderr 丢失 - 进程池关闭后,未完成的
Future不会自动取消;显式调用executor.shutdown(wait=False)+future.cancel(),但注意:已进入子进程执行的任务无法中断
最常被忽略的一点:multiprocessing 的性能拐点不在代码逻辑,而在数据序列化成本和进程间通信模式。别急着加核数,先用 cProfile 和 psutil 看清瓶颈到底在 pickle、I/O 还是纯计算。









