Python multiprocessing Manager进程父进程停止后未退出问题
multiprocessing.Manager残留进程与内存泄漏问题 问题描述
我在Python 3.10.12环境下编写了演示脚本,复现复杂项目中的多进程处理逻辑(核心逻辑已包含)。通过CTRL+C停止脚本时,偶尔会出现内存泄漏,Linux下用htop可见僵死进程占用内存。我已确认所有进程终止且退出码为-15(对应CTRL+C),但内存泄漏问题仍存在。
演示脚本
import multiprocessing as mp import time class ProcessManager: def __init__(self) -> None: self.proc_map = {} self.map_procs() def map_procs(self): for i in range(1): self.proc_map[i] = mp.Process(target=self.parent_loop) print("mapped processes in orchestrator") def start_procs(self): # Start all processes for i, proc in self.proc_map.items(): proc.start() print(f"Parent process {i} started.") # wait for processes to finish for _, proc in self.proc_map.items(): try: proc.join() # wait for the process to finish (it may or may not, depending on loops) except KeyboardInterrupt: for _, proc in self.proc_map.items(): print(f"Terminating process {proc}...") proc.terminate() for _, proc in self.proc_map.items(): proc.join() # wait for termination to finish def parent_loop(self): p = Parent() p.parent_loop() class Parent: def __init__(self) -> None: self.queue = mp.Manager().Queue() self.child = ChildDaemon(self.queue) self.dummy_file_in_memory = self.load_big_file() self.slow_init_func(5) self.child.daemon_proc.start() def slow_init_func(self, t): print("slow init func") time.sleep(t) def load_big_file(self): file_size = 1024 * 1024 * 1024 * 20 dummy_data = bytearray(file_size) return dummy_data def parent_loop(self): while True: print(f"parent loop: queue counter {self.queue.get()}") time.sleep(5) class ChildDaemon: def __init__(self, queue) -> None: self.queue = queue self.daemon_proc = mp.Process(target=self.child_loop, args=(queue,)) self.daemon_proc.daemon = True def child_loop(self, queue): counter = 0 while True: print("child loop") time.sleep(1) try: queue.get_nowait() except: pass try: queue.put(counter, block=False) except: pass counter += 1 if __name__=="__main__": mp.set_start_method('spawn') orch = ProcessManager() orch.start_procs() for key, proc in orch.proc_map.items(): print("Checking exitcodes of supervisor processes...") print(f"{key} ({proc}) exitcode: {proc.exitcode}")
执行情况
在Mac OS 14.1.2和Python 3.9.12环境下,按CTRL+C退出脚本后,输出如下:
mapped processes in orchestrator Parent process 0 started. slow init func child loop child loop parent loop: queue counter 0 child loop child loop child loop child loop parent loop: queue counter 4 child loop child loop ^CTerminating process <Process name='Process-1' pid=13652 parent=13650 started>... Process Process-1:2: Traceback (most recent call last): File "/Users/******/opt/anaconda3/lib/python3.9/multiprocessing/process.py", line 315, in _bootstrap self.run() File "/Users/******/opt/anaconda3/lib/python3.9/multiprocessing/process.py", line 108, in run self._target(*self._args, **self._kwargs) File "/Users/******/Projects/processes_leak/src.py", line 74, in child_loop time.sleep(1) KeyboardInterrupt Checking exitcodes of supervisor processes... 0 (<Process name='Process-1' pid=13652 parent=13650 stopped exitcode=-SIGTERM>) exitcode: -15
此时htop显示仍有残留进程。
补充测试
经排查怀疑问题出在mp.Manager()调用,简化后的测试脚本执行完毕后也可见残留Python进程:
import multiprocessing as mp import time class ProcessManager: def __init__(self): self.organise() def organise(self): p = mp.Process(target=self.start_manager) p.start() try: p.join() except KeyboardInterrupt: print('pm exception') try: self.m.shutdown() except: pass p.terminate() p.join() def start_manager(self): self.m = mp.Manager() self.q = self.m.Queue() try: time.sleep(10) finally: print('start_manager exception') self.m.shutdown() if __name__ == "__main__": pm = ProcessManager()
疑问
现寻求无需使用上下文管理器的Manager进程正确关闭方案,同时有疑问:根据Python 3.9官方文档,Manager进程会在垃圾回收或父进程退出时自动关闭,为何仍会出现残留?
解决方案
1. 显式持有Manager实例并主动关闭
避免匿名创建Manager,显式保存实例以便主动控制生命周期。修改Parent类的初始化逻辑:
class Parent: def __init__(self) -> None: self.manager = mp.Manager() # 显式保存Manager实例 self.queue = self.manager.Queue() # 其余代码保持不变
在捕获KeyboardInterrupt时,需要确保调用self.manager.shutdown()。可以在Parent类中添加关闭方法,或者在进程终止前传递manager引用进行清理。
2. 优化信号处理流程
当捕获到CTRL+C(SIGINT)时,先触发Manager的shutdown(),再终止子进程,最后等待所有进程退出。避免直接用terminate()强制杀死父进程,导致Manager进程来不及清理。
3. 调整Manager创建位置(可选)
尽量在主进程中创建Manager,再将共享队列传递给子进程。这样主进程可以直接管理Manager的生命周期,减少子进程意外终止带来的残留风险。
文档描述与实际残留的原因
- 官方文档中“
Manager进程会在垃圾回收或父进程退出时自动关闭”的前提是父进程正常退出,而非被强制信号(如SIGTERM)终止。当父进程被terminate()杀死时,Python解释器可能来不及执行Manager实例的析构函数,导致Manager进程成为孤儿进程残留。 - 匿名创建的
Manager实例(如mp.Manager().Queue())会失去对Manager进程的引用,垃圾回收器可能无法及时触发清理逻辑,尤其是在多进程并发场景下。
内容的提问来源于stack exchange,提问作者John Karkas

