垃圾收集器尝试收集共享内存对象

时间:2017-03-29 09:49:16

标签: python unix garbage-collection multiprocessing fork

我有两个Python脚本,两者都应该做同样的事情:在内存中抓取一个大对象,然后分叉一堆孩子。第一个脚本使用裸os.fork

import time
import signal
import os
import gc

gc.set_debug(gc.DEBUG_STATS)


class GracefulExit(Exception):
    pass


def child(i):
    def exit(sig, frame):
        raise GracefulExit("{} out".format(i))

    signal.signal(signal.SIGTERM, exit)
    while True:
        time.sleep(1)


if __name__ == '__main__':
    workers = []

    d = {}
    for i in xrange(30000000):
        d[i] = i

    for i in range(5):
        pid = os.fork()
        if pid == 0:
            child(i)
        else:
            print pid
            workers.append(pid)

    while True:
        wpid, status = os.waitpid(-1, os.WNOHANG)
        if wpid:
            print wpid, status
        time.sleep(1)

第二个脚本使用multiprocessing模块。我在Linux上运行(Ubuntu 14.04),所以它也应该使用os.fork,因为documentation表示:

import multiprocessing
import time
import signal
import gc

gc.set_debug(gc.DEBUG_STATS)


class GracefulExit(Exception):
    pass


def child(i):
    def exit(sig, frame):
        raise GracefulExit("{} out".format(i))

    signal.signal(signal.SIGTERM, exit)
    while True:
        time.sleep(1)


if __name__ == '__main__':
    workers = []

    d = {}
    for i in xrange(30000000):
        d[i] = i

    for i in range(5):
        p = multiprocessing.Process(target=child, args=(i,))
        p.start()
        print p.pid
        workers.append(p)

    while True:
        for worker in workers:
            if not worker.is_alive():
                worker.join()
        time.sleep(1)

这两个脚本之间的区别如下:当我杀死一个孩子(发送SIGTERM)时,裸叉脚本尝试垃圾收集共享字典,尽管它仍然被父进程引用而不是实际上被复制到孩子的记忆中(因为写时复制)

kill <pid>

Traceback (most recent call last):
  File "test_mp_fork.py", line 33, in <module>
    child(i)
  File "test_mp_fork.py", line 19, in child
    time.sleep(1)
  File "test_mp_fork.py", line 15, in exit
    raise GracefulExit("{} out".format(i))
__main__.GracefulExit: 3 out
gc: collecting generation 2...
gc: objects in each generation: 521 3156 0
gc: done, 0.0024s elapsed.

perf record -e page-faults -g -p <pid>输出:)

+  99,64%  python  python2.7           [.] PyInt_ClearFreeList
+   0,15%  python  libc-2.19.so        [.] vfprintf
+   0,09%  python  python2.7           [.] 0x0000000000144e90
+   0,06%  python  libc-2.19.so        [.] strlen
+   0,05%  python  python2.7           [.] PyArg_ParseTupleAndKeywords
+   0,00%  python  python2.7           [.] PyEval_EvalFrameEx
+   0,00%  python  python2.7           [.] Py_AddPendingCall
+   0,00%  python  libpthread-2.19.so  [.] sem_trywait
+   0,00%  python  libpthread-2.19.so  [.] __errno_location

虽然基于多处理的脚本没有这样做:

kill <pid>

Process Process-3:
Traceback (most recent call last):
  File "/usr/lib/python2.7/multiprocessing/process.py", line 258, in _bootstrap
    self.run()
  File "/usr/lib/python2.7/multiprocessing/process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "test_mp.py", line 19, in child
    time.sleep(1)
  File "test_mp.py", line 15, in exit
    raise GracefulExit("{} out".format(i))
GracefulExit: 2 out

perf record -e page-faults -g -p <pid>输出:)

+  62,96%  python  python2.7           [.] 0x0000000000047a5b
+  32,28%  python  python2.7           [.] PyString_Format
+   2,65%  python  python2.7           [.] Py_BuildValue
+   1,06%  python  python2.7           [.] PyEval_GetFrame
+   0,53%  python  python2.7           [.] Py_AddPendingCall
+   0,53%  python  libpthread-2.19.so  [.] sem_trywait

通过在提升gc.collect()之前显式调用GracefulExit,我还可以强制在基于多处理的脚本上执行相同的行为。奇怪的是,相反的情况并非如此:在裸叉脚本中调用gc.disable(); gc.set_threshold(0)无助于摆脱PyInt_ClearFreeList次调用。

对实际问题:

  • 为什么会这样?我有点理解为什么python想要在进程退出时释放所有已分配的内存,忽略了子进程没有物理拥有它的事实,但是多处理模块怎么不这样做呢?
  • 我想通过裸叉解决方案实现类似第二脚本的行为(即:不尝试释放由父进程分配的内存)(主要是因为我使用第三方进程管理器库它不使用多处理);我怎么可能这样做?

2 个答案:

答案 0 :(得分:0)

情侣

  • 在python中,多个python 进程意味着多个解释器拥有自己的GIL,GC等

  • d字典不作为参数传递给进程,它是一个全局共享变量。

收集它的原因是因为每个进程认为它是唯一一个持有对它的引用的进程,严格来说,它是真的,因为它是对字典的单个全局共享对象引用。

当Python GC检查它时,它会检查该对象的ref计数器。由于只有一个共享引用,删除它意味着ref count == 0,因此它会被收集。

要解决此问题,应将d传递到每个分叉进程,使每个进程都拥有自己的引用。

答案 1 :(得分:0)

多处理的行为有所不同,因为它使用的os._exit不会调用退出处理程序,这显然涉及垃圾收集(more on the topic)。在脚本的裸叉版本中明确调用os._exit可以获得相同的结果。