python multiprocessing
### 1. 他是什么
Python的multiprocessing模块,说白了就是用来绕过GIL(全局解释器锁)的那把钥匙。CPython的GIL一次只允许一个线程执行字节码,所以多线程在CPU密集型任务上基本是摆设。而multiprocessing通过创建独立的进程——每个进程都有自己的Python解释器和内存空间——直接绕开了这个限制。你可以把它想象成开了一家火锅店:一个线程就像只有一个厨师在厨房里忙活,哪怕你雇了十个服务员,灶台只有一口锅,效率依然上不去。而multiprocessing相当于让每个厨师都有自己的灶台和锅,食材(数据)虽然不能随便共享,但烹饪速度确实能翻倍。
2. 他能做什么
他能把CPU密集型任务做到真正并行。比如图像处理、视频编解码、大规模数值计算——这些场景下,如果你用多线程,四个CPU核心可能只跑出一个核心的利用率;但是用multiprocessing,四个核心能接近满载。此外,他还能隔离崩溃风险。假如你写了一个可能因为第三方库的C扩展而段错误的程序,用一个进程跑任务,崩了也只是这个进程退出,主程序和其他进程不受影响。这比threading模块安全得多,因为线程崩溃往往是整锅端。
但注意,他并不是万能的。如果是IO密集型任务(网络请求、文件读写),多线程反而更划算。因为进程的创建开销比线程大得多,而且进程间通信(IPC)需要序列化数据(比如用Queue或Pipe),这会带来额外的性能损耗。对于大多数Web后端场景,asyncio或threading才是更自然的选型。
3. 怎么使用
一个常见的例子是处理大量图片。假设有1000张图片需要调整尺寸,单线程可能需要10秒,而用四进程能把时间压到3秒左右。基础用法是这样的:
frommultiprocessingimportPooldefprocess_image(filename):# 模拟图像处理returnf"{filename}processed"if__name__=="__main__":filenames=[f"img_{i}.jpg"foriinrange(1000)]withPool(processes=4)aspool:results=pool.map(process_image,filenames)这里Pool就像一个工头,把任务分给四个工人进程。pool.map会阻塞主程序直到所有任务完成。如果要更精细控制,可以用apply_async配合回调函数。
如果任务之间有依赖关系,你可能需要直接操作Process类:
frommultiprocessingimportProcess,Queuedefworker(q,data):result=data*2q.put(result)if__name__=="__main__":q=Queue()p=Process(target=worker,args=(q,10))p.start()value=q.get()p.join()这里用Queue传递结果是个好习惯,因为它默认做了序列化和进程安全。直接共享内存(比如Value或Array)虽然快,但容易踩到同步问题的坑——除非你非常清楚自己在做什么。
4. 最佳实践
第一,永远把资源初始化放在if __name__ == "__main__":后面。Windows上没有fork机制,会重新导入模块,如果不加这个保护,每个子进程创建时都会莫名其妙地启动新的进程池,形成无限递归。这种错误能把调试体验拉低到地狱级别。
第二,优先用Pool而不是自己管理进程生命周期。Pool自动维护进程池、处理任务分配。手动管理Process就像自己写线程调度器一样,容易在异常处理、资源回收上出问题。比如进程因为异常退出时,你得手动检查退出码;但Pool会帮你重新启动新进程。
第三,需要共享数据时,用Manager。普通变量在子进程中是独立副本,改动了父进程也看不见。Manager提供了一种代理对象,比如Manager().list(),虽然每次访问都有序列化开销,但比手撸Pipe和锁要安全得多。如果对性能有极致要求(比如传递numpy数组),可以研究shared_memory模块,但日常开发还是少碰为妙。
第四,记得设置maxtasksperchild。当子进程处理大量任务时,可能会积累内存泄漏(比如第三方库的C扩展缓慢泄露)。设置Pool(processes=4, maxtasksperchild=100)可以在每100个任务后重启进程,像定期的垃圾回收一样。
5. 和同类技术对比
对比多线程(threading):多线程的创建开销低(约50KB内存),进程则要数MB;多线程共享内存、无序列化开销,但受GIL限制只能利用单核。所以,CPU密集任务用multiprocessing,IO密集用threading。有一个灰色地带:混合场景(比如先读文件再计算),可以用多线程处理IO部分,再把计算结果通过concurrent.futures提交给进程池。不过这种方案容易让代码变得像意大利面条一样,还不如直接用asyncio的run_in_executor来解耦。
对比**concurrent.futures.ProcessPoolExecutor**:这实际上是multiprocessing.Pool的封装,接口更统一,支持as_completed等高级用法。但它的灵活性稍差——比如无法设置maxtasksperchild,也不能直接访问Queue。如果只是做简单的map操作,用它更优雅;需要精细控制时,还是需要回到multiprocessing。
对比**subprocess**:subprocess是启动外部程序(比如ffmpeg),进程间通过标准输入输出通信。而multiprocessing启动的是Python解释器中的函数。如果你的任务已经有一个现成的命令行工具(比如ImageMagick的convert),那用subprocess调用比重新用Python写一遍要高效得多。但如果是自定义的计算逻辑,显然multiprocessing更自然。
对比分布式框架(如Ray):Ray做的其实就是把multiprocessing的概念扩展到多台机器上。如果你只有单台服务器,multiprocessing更轻量;如果服务器有几十核心甚至跨机器,Ray能自动处理序列化、调度和容错。不过Ray的学习曲线比multiprocessing陡峭得多,项目中要权衡好是否真的需要这种规模。
