文章目录
一、简介
这里简单介绍在Python 中使用多进程编程的时候容易遇到的情况和解决办法,有助于排查和规避某类问题,但是具体问题还是需要具体分析,后续会补充更多的内容。
二、选择合适的启动方式
-
Python 的多进程编程 非常需要注意启动方式,有些库会在
fork
模式下出现很诡异的阻塞或显存泄漏。如果需要跨平台使用,优先选择spawn
方式,如果涉及到 GPU、PyTorch、OpenCV 视频流、TensorRT、OpenVINO ,同样优先选择spawn
方式,避免继承GPU上下文。有的时候会存在 多线程 和 多进程 混用的情况,可以选择spawn
或forkserver
这两种方式。spawn
启动方式通常更安全、更稳定。启动方式 平台默认 机制 优点 缺点 适用场景 fork Linux / macOS 子进程直接复制父进程的内存空间(写时复制),从 fork()
返回处继续执行创建速度快,占用内存少 继承父进程线程状态可能死锁,继承 GPU/文件句柄可能出错,不支持 Windows 纯计算、多进程间状态共享简单、无 GPU/大文件句柄的场景 spawn Windows 启动全新的 Python 解释器进程,从头执行 __main__
模块的代码跨平台一致,状态干净,不继承父进程线程/句柄/GPU context 启动慢,初始化开销大,需要 if __name__ == "__main__":
GPU 深度学习、跨平台项目、多线程+多进程混合、需要隔离状态的场景 forkserver Linux / macOS 先启动一个 "fork server" 进程,之后所有子进程由它 fork
出来避免从主进程直接 fork(减少死锁风险),比 spawn 快 只在类 Unix 系统可用,需额外启动 server,复杂度高 多线程+多进程并存、需要避免主进程 fork 的高并发服务 -
可以通过调用
multiprocessing.set_start_method('spawn')
来指定子进程的启动方式,通常写在if __name__ == "__main__":
里,force=True
参数可以强制重置启动方式(但通常不建议随便用,除非确定没别的进程创建了子进程)。pythonimport multiprocessing as mp def worker(name): print(f"Worker {name} running...") if __name__ == "__main__": # 强制统一 spawn mp.set_start_method("spawn", force=True) # "fork" 或 "forkserver" processes = [] for i in range(3): p = mp.Process(target=worker, args=(i,)) p.start() processes.append(p) for p in processes: p.join()
-
使用 PyAV 调用
h264_qsv
(Intel Quick Sync Video)硬件加速解码时,也会受多进程启动方式的影响,QSV 是基于英特尔硬件加速的编码解码技术,需要在进程中正确初始化硬件上下文。如果使用fork
,子进程会复制父进程的内存空间,但硬件设备上下文(如 QSV 的硬件句柄、驱动状态等)通常不能被正确继承或共享,导致解码失败、死锁或崩溃。使用spawn
,子进程是全新启动,独立初始化硬件上下文,能避免因上下文继承带来的问题,提高稳定性。多进程启动方式 对 PyAV + h264_qsv 硬解码的影响 推荐做法 fork 可能导致硬件上下文冲突,解码异常 不推荐 spawn 子进程独立初始化硬件上下文,稳定性高 推荐,尤其多进程并发的时候 -
在使用
fork
启动多进程的时候,部分常用的 计算机视觉 (CV)和 点云处理 相关库可能会出现问题,任何涉及 GPU上下文、线程池或多线程加速的库,在fork
多进程启动方式下都可能出现资源继承异常、死锁、崩溃等问题。点云库中,Open3D
和PCL
较为复杂,fork
时要特别注意。库/模块 说明与常见问题 OpenCV (cv2) GPU模式(CUDA)在fork后可能资源异常或崩溃,多线程环境下死锁;CPU模式一般安全,但多线程仍需注意。 Open3D 内部使用线程池和GPU(部分功能),fork启动时可能导致线程池状态异常或GPU资源不可用。 PCL / python-pcl 底层多线程和GPU支持(部分平台),fork后可能出现线程死锁或资源冲突,尤其在GPU加速时。 PyTorch fork后CUDA上下文继承出错,导致报错、卡死;多线程资源也可能异常。 TensorFlow fork后session、资源无法正常初始化,多线程管理导致崩溃。 OpenVINO 线程池和设备初始化受fork影响,可能导致设备资源冲突或初始化失败。 TensorRT GPU上下文和优化缓存fork后不稳定,可能导致初始化失败或运行错误。 matplotlib GUI线程在fork后可能卡死或异常。
三、手动终止所有的进程
-
多进程中
Ctrl+C
(SIGINT
信号)后程序没有全部退出,最常见原因就是信号没有传递给子进程,或者子进程没能响应退出请求。fork
导致资源继承异常也经常是根源。列出当前系统中所有正在运行的 Python 进程及其详细信息。bashps aux | grep python
-
在多进程场景下,
torch.utils.data.DataLoader
是常见的导致进程无法退出的元凶之一,尤其是在num_workers > 0
时。有的情况默认不监听结束信号,有的情况收不到结束信号。显式处理可能比较麻烦。 -
一个比较安全的写法是在主进程 捕获
SIGINT
,然后主动杀掉自己启动的所有子进程。直接一次性结束整个进程树。pythonimport multiprocessing as mp import signal import sys import os import psutil def kill_all_children(timeout=3): """ 终止当前进程的所有子进程(递归),先尝试终止,再强制杀死超时未结束的子进程 """ proc = psutil.Process(os.getpid()) children = proc.children(recursive=True) # 尝试终止 for child in children: try: child.terminate() except Exception as e: print(f"[警告] 无法终止 PID={child.pid}: {e}", file=sys.stderr) # 等待超时,获取仍然存活的子进程 gone, alive = psutil.wait_procs(children, timeout=timeout) # 强制杀死仍然存活的进程 for child in alive: try: child.kill() except Exception as e: print(f"[错误] 无法强制终止 PID={child.pid}: {e}", file=sys.stderr) def signal_handler(sig, frame): # 回调函数 print(f"\n[退出] 捕获信号 {sig},终止子进程并退出...") try: kill_all_children() except Exception as e: print(f"[错误] 终止子进程时出错: {e}", file=sys.stderr) finally: sys.exit(130 if sig == signal.SIGINT else 143) # 130 = Ctrl+C 退出, 143 = SIGTERM 退出 def main(args): # 启动多个进程的示例 processes = [] for _ in range(4): p = mp.Process(target=worker_function) p.start() processes.append(p) for p in processes: p.join() if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) # 注册信号和回调函数 signal.signal(signal.SIGTERM, signal_handler) mp.set_start_method('spawn', force=True) args = parse_args() main(args)
-
或者更简单直接的立即杀掉所有进程,终端更干净。可以根据实际情况考虑如何使用。
pythonimport multiprocessing as mp import signal import sys import os import psutil def kill_all_processes(): proc = psutil.Process(os.getpid()) for child in proc.children(recursive=True): child.kill() proc.kill() def signal_handler(sig, frame): print("\n[退出] 捕获 Ctrl+C,终止所有进程...") kill_all_processes() sys.exit(0) def main(args): # 启动多个进程的示例 processes = [] for _ in range(4): p = mp.Process(target=worker_function) p.start() processes.append(p) for p in processes: p.join() if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) mp.set_start_method('spawn', force=True) args = parse_args() main(args)
小结
以上内容来自相关资料和个人实践,具体情况还请具体分析,可以参考这里提到的几个关键点,有助于排查问题,如有问题欢迎在评论区指正,谢谢!!