Python 多进程退出与清理机制

1次阅读

atexit注册函数仅在当前进程正常退出时触发,不跨进程生效;子进程崩溃或被信号终止时,其atexit回调不会执行,必须用try/except/finallysignal.signal显式处理清理。

Python 多进程退出与清理机制

子进程崩溃时主进程不退出?atexit 失效的真相

pythonatexit 注册函数只在当前进程正常退出(如 sys.exit()、脚本自然结束)时触发,**不会跨进程生效**。多进程场景下,子进程崩溃或被 os.kill() 终止,其内部注册的 atexit 回调根本不会执行。

常见错误现象:你在子进程中用 atexit.register(cleanup_tmp),但子进程被 signal.SIGTERM 杀掉后,临时文件还在;或者子进程因未捕获异常直接退出,atexit 彻底静默。

  • 真正可靠的清理必须放在子进程自己的异常处理链路里,比如 try/except/finally 块中
  • 若依赖信号退出,需显式注册 signal.signal(signal.SIGTERM, handler) 并在 handler 里调用清理逻辑
  • multiprocessing.Processterminate() 方法不给子进程任何执行清理的机会,它等价于 os.kill(..., SIGKILL)

multiprocessing.Pool 关闭后资源没释放?别只调 close()

Pool.close() 只是禁止提交新任务,已提交任务仍在运行;Pool.terminate() 强制杀掉所有工作进程,但两者都不等待子进程真正退出——这意味着主进程可能提前结束,而子进程残留的文件句柄、锁、共享内存段还卡在系统里。

使用场景:跑完一批 CPU 密集任务后要立刻释放 GPU 显存、删掉临时 mmap 文件、关闭数据库连接池。

立即学习Python免费学习笔记(深入)”;

  • 必须配对使用:pool.close(); pool.join() 才能确保所有子进程结束
  • pool.join() 会阻塞主进程,直到所有工作进程退出;若子进程卡死,主进程就永远 hang 住
  • 更安全的做法是加超时:pool.join(timeout=30),超时后手动 pool.terminate() 再重试
  • 注意:join() 前必须先 close()terminate(),否则抛 ValueError

子进程里用 threading.Lock 会死锁?共享对象的陷阱

多进程不是线程threading.Lockthreading.Event 等线程同步原语**不能跨进程共享**。把它们作为参数传给 Process 或放进 Manager,实际发生的是 pickle + 反序列化,得到的是新对象副本,完全无法协调多个进程。

常见错误现象:多个子进程同时写一个日志文件,以为加了 threading.Lock 就安全,结果日志还是乱序或损坏;或者用 Manager().dict() 存状态,却用线程锁去保护它,纯属无效操作。

  • 进程间同步必须用 multiprocessing.Lockmultiprocessing.Semaphoremultiprocessing.Event
  • Manager() 返回的对象(如 dictlist)本身已做进程安全封装,一般不需要额外加锁
  • 如果要用文件做协调,优先考虑 os.open(..., os.O_EXCL | os.O_CREAT) 这类原子操作,而不是靠锁

windows 下子进程继承句柄导致主进程无法退出

Windows 默认将主进程的打开句柄(文件、socket、pipe)**继承给子进程**。如果子进程没显式关闭这些句柄,主进程调用 process.join()pool.close() 后,仍可能因句柄被占用而无法彻底退出——尤其常见于日志文件、数据库连接、子进程启动的 subprocess。

性能影响:句柄泄漏会拖慢进程销毁速度,严重时触发系统句柄数限制,后续 Process 创建失败,报错 OSError: [WinError 87] The parameter is incorrect

  • 启动子进程时加 close_fds=True(Python 3.7+ 默认为 True,但旧版本或某些封装库可能关掉)
  • 子进程入口函数开头加 import multiprocessing; multiprocessing.set_start_method('spawn', force=True),避免 fork 类行为残留
  • 检查是否用了 subprocess.Popen(..., stdout=xxx) 把主进程的 sys.stdout 传下去——这会让子进程持有一个指向主进程 stdout 的句柄

事情说清了就结束。最麻烦的永远不是“怎么写”,而是“哪个清理动作该在哪一层做”——主进程、子进程、甚至子进程里的 subprocess,各自的生命周期和资源归属得掰扯清楚,不然总有一处漏网。

text=ZqhQzanResources