You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 22:47:34