Python的多进程居然把我坑惨了!别踩这个坑
免费编程软件「pythonpycharm」链接https://pan.quark.cn/s/48a86be2fdc0一个在Windows上跑得好好的代码上了服务器就崩了去年有个项目我需要并行处理一批数据。在Windows笔记本上写完代码测试一切正常from multiprocessing import Process def worker(name): print(f进程{name}开始工作) p1 Process(targetworker, args(A,)) p2 Process(targetworker, args(B,)) p1.start() p2.start() p1.join() p2.join()输出完美进程A开始工作 进程B开始工作然后我把代码部署到Linux服务器上运行报错AttributeError: Process object has no attribute _popen我当时就懵了。同样的代码换个环境就崩了上网一查发现这个错误很常见而且原因让人很无语操作系统不同Python多进程的底层实现不一样。从那天开始我算是把Python多进程的坑踩了个遍。今天把这些坑和绕坑方法写出来希望能帮你省下几个调试的夜晚。坑1不同操作系统多进程行为完全不同Python的multiprocessing模块号称统一接口实际上底层实现差异巨大。**Linux/macOS下默认用fork**子进程是父进程的克隆所有已加载的模块和变量直接复制过去不需要重新导入。**Windows下只能用spawn**Windows没有fork系统调用必须启动一个新的Python解释器重新执行所有导入代码。这意味着你的代码如果在Windows上跑得通在Linux上不一定反过来也一样。典型症状AttributeError: Process object has no attribute _popen这个错误通常是因为没有加if __name__ __main__保护。Windows下需要用spawn启动子进程会重新导入主模块。如果没有主模块保护子进程会无限递归创建新进程。绕坑指南# 正确写法——永远用if __name__ __main__保护 from multiprocessing import Process def worker(): print(工作中) if __name__ __main__: # 这行必须有 p Process(targetworker) p.start() p.join()如果你在Windows下运行代码没有任何输出只打印了Done!很可能就是这个原因。坑2全局变量在子进程里消失了我写过这样的代码config {mode: fast} def worker(): print(config[mode]) # 想读全局配置 if __name__ __main__: p Process(targetworker) p.start() p.join()在Linux下用fork方式启动子进程复制了父进程的内存config还在能正常读取。但如果在Windows或者设置了spawn的Linux上运行子进程会重新导入模块会创建新的config对象值对得上就用对不上就出问题。绕坑指南不要把依赖全局状态的代码放到子进程里把需要的参数通过函数参数显式传递如果需要多进程共享数据用multiprocessing.Manager或Queue# 正确做法 def worker(config_mode): # 通过参数传递 print(config_mode) if __name__ __main__: config {mode: fast} p Process(targetworker, args(config[mode],)) p.start() p.join()坑3自定义类和函数不能被pickle这个坑出现在用进程池Pool的时候。from multiprocessing import Pool class Calculator: def compute(self, x): return x * x def run(): calc Calculator() with Pool(2) as pool: results pool.map(calc.compute, [1, 2, 3]) # 报错报错信息PicklingError: Cant pickle class __main__.Calculator原因multiprocessing需要把函数和参数序列化pickle后传给子进程。如果对象无法被pickle进程间通信就失败了。绕坑指南尽量用基本类型int、str、list、dict作为参数如果一定要传自定义对象考虑在子进程内部创建# 正确做法 def compute(x): return x * x with Pool(2) as pool: results pool.map(compute, [1, 2, 3]) # 用函数不用对象方法坑4进程池里的任务静默失败进程池的map方法有个问题如果某个子进程崩溃了它不会报错只是卡住或返回不完整的结果。from multiprocessing import Pool def risky_task(x): if x 2: raise ValueError(出错了) # 这个异常不会直接抛出来 return x * 2 with Pool(2) as pool: results pool.map(risky_task, [1, 2, 3]) print(results) # 你猜会不会报错结果程序卡住或报错MaybeEncodingError但真正的异常信息丢失了。绕坑指南在子进程函数内部捕获所有异常把错误信息作为返回值返回。def safe_task(x): try: return {success: True, result: x * 2} except Exception as e: return {success: False, error: str(e)}坑5多进程多线程死锁风险如果你在多线程环境里创建子进程Python的fork会复制父进程的所有线程状态但只有执行fork的那个线程被保留。这就可能导致一个严重后果如果其他线程在fork时持有锁子进程里这个锁的状态被复制了但持有锁的线程并不存在于是子进程永远等不到锁释放——死锁。Python 3.4到3.6之间的版本还有个bug用fork创建的子进程里主线程会直接调用os._exit()退出不会等待其他线程完成。绕坑指南尽量不要同时使用多进程和多线程如果非要用先创建进程在进程里再创建线程在Python 3.14Linux默认会用forkserver替代fork能缓解这个问题坑6spawn方式下代码被重新执行当你用spawn方式启动子进程时Windows默认Linux可选子进程会启动一个新的Python解释器重新执行所有模块级代码。如果你的模块级代码有副作用比如初始化GPU、连接数据库、启动服务在spawn模式下会重复执行导致各种诡异问题。绕坑指南# 把所有初始化代码放到if __name__ __main__里面 if __name__ __main__: # 初始化GPU、连接数据库等代码放这里 import torch torch.npu.set_device(0) # 不要在模块顶层做这个 # 然后再启动多进程 from multiprocessing import Process p Process(targetworker) p.start()一张表对比三种启动方式启动方式Linux/macOSWindows特点风险fork默认3.14前不支持最快复制父进程内存多线程环境可能死锁spawn可选默认安全启动慢会重新导入所有模块forkserver默认3.14不支持折中方案同样有spawn的重新导入问题怎么选import multiprocessing as mp # 查看当前默认方式 print(mp.get_start_method()) # 手动设置启动方式必须在创建进程之前 if __name__ __main__: mp.set_start_method(spawn) # 跨平台兼容性最好 # 然后创建进程...最后的建议Python多进程的这些问题核心原因是跨平台兼容性和进程间通信机制的复杂性。如果你的项目只需要在Linux上跑用默认的fork问题不大但要小心多线程场景。如果需要跨平台统一用spawnif __name__ __main__保护虽然启动慢一点但至少行为一致、不容易出bug。记住这个公式多进程代码 if __name__保护 显式传递参数不用全局变量 避免pickle不兼容的对象这条公式能帮你绕过绝大多数Python多进程的坑。