当前位置: 首页 > news >正文

Python的多进程居然把我坑惨了!别踩这个坑

免费编程软件「python+pycharm」
链接:https://pan.quark.cn/s/48a86be2fdc0

一个在Windows上跑得好好的代码,上了服务器就崩了

去年有个项目,我需要并行处理一批数据。在Windows笔记本上写完代码,测试一切正常:

from multiprocessing import Process def worker(name): print(f"进程{name}开始工作") p1 = Process(target=worker, args=("A",)) p2 = Process(target=worker, 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(target=worker) p.start() p.join()

如果你在Windows下运行代码没有任何输出(只打印了"Done!"),很可能就是这个原因。


坑2:全局变量在子进程里"消失"了

我写过这样的代码:

config = {"mode": "fast"} def worker(): print(config["mode"]) # 想读全局配置 if __name__ == "__main__": p = Process(target=worker) p.start() p.join()

在Linux下,用fork方式启动,子进程复制了父进程的内存,config还在,能正常读取。

但如果在Windows或者设置了spawn的Linux上运行,子进程会重新导入模块,会创建新的config对象,值对得上就用,对不上就出问题。

绕坑指南

  • 不要把依赖全局状态的代码放到子进程里

  • 把需要的参数通过函数参数显式传递

  • 如果需要多进程共享数据,用multiprocessing.ManagerQueue

# 正确做法 def worker(config_mode): # 通过参数传递 print(config_mode) if __name__ == "__main__": config = {"mode": "fast"} p = Process(target=worker, 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: Can't 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.14+,Linux默认会用forkserver替代fork,能缓解这个问题


坑6:spawn方式下代码被"重新执行"

当你用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(target=worker) 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问题不大,但要小心多线程场景。如果需要跨平台,统一用spawn+if __name__ == "__main__"保护,虽然启动慢一点,但至少行为一致、不容易出bug。

记住这个公式:

多进程代码 = if __name__保护 + 显式传递参数(不用全局变量) + 避免pickle不兼容的对象

这条公式能帮你绕过绝大多数Python多进程的坑。

http://www.jsqmd.com/news/1076431/

相关文章:

  • 3步快速解决Jellyfin中文影视刮削难题:MetaShark完整配置教程
  • 别再瞎找了!盘点2026年万众偏爱的的AI论文平台
  • 致远FE平台apprvaddNew接口SQL注入漏洞挖掘与防御实践
  • Detecting hallucinations in large language models using semantic entropy
  • AI离题(Digression)识别与防御实战指南
  • PDF转Word教程:3款免费在线工具实测(支持OCR识别与批量转换)
  • 如何在家中搭建游戏串流服务器?Sunshine让你随时随地畅玩PC游戏
  • 聚类实战指南:从无监督学习到业务可解释的工程落地
  • 从产品设计角度看「适趣古诗词」的分级与复习机制
  • 利用LPCUSBSIO库实现免驱USB转I2C通信:跨平台开发与实战指南
  • 泛微e-Bridge任意文件上传漏洞深度剖析与安全防御实践
  • LCS4110R—为您的智能设备赋予金融级的安全防护
  • 什么是 OpenAI Codex?
  • 如何高效使用Ryujinx:免费开源的Nintendo Switch模拟器完整指南
  • Log4j2漏洞深度解析:从JNDI注入原理到企业级应急响应实战
  • 思源宋体终极指南:如何在5分钟内免费获得专业级中文字体
  • ALINX 慕尼黑上海电子展邀请函|免费咖啡、特斯拉香薰、定制周边+千元板卡大奖等你来拿!​
  • 边缘计算场景下的时序数据库选型:TDengine 边缘版实战
  • 面向 MQL4 / MQL5 策略代码的 AI 辅助生成与编译校验工作流实践
  • 从CVE-2024-0517与CVE-2024-6507看Chrome RCE漏洞的攻防实战
  • 从线性回归到Transformer:统计视角下的条件概率建模演进
  • Make-a-Video实战指南:文本生成视频的原理、调优与工作流集成
  • 私域电商系统避坑指南
  • 神经酸PS-DHA脑力工作者的营养真相
  • CVE-2025-32395漏洞剖析:Vite开发服务器路径遍历与安全加固实战
  • Outfit字体完整指南:9种字重开源几何无衬线字体如何重塑现代品牌视觉系统
  • Django计算机毕设之基于 Django 的毕业生求职岗位精准推荐系统设计与实现 基于 Django 的就业资源智能推送信息系统(完整前后端代码+说明文档+LW,调试定制等)
  • AI时代内容创作工业化:从小说到漫剧,普通人也能打造自己的IP宇宙
  • Dreamer模型驱动强化学习实战:从世界模型到机械臂部署
  • m4s-converter:B站视频格式转换完整指南,让缓存视频永久留存