繁体   English   中英

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

[英]Garbage collector tries to collect shared memory object

我有两个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使用os.fork ,如文档所述:

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

我还可以通过在引发GracefulExit之前显式调用gc.collect()在基于多处理的脚本上强制执行相同的行为。 奇怪的是,事实并非如此:调用gc.disable(); gc.set_threshold(0) 裸机脚本中的gc.disable(); gc.set_threshold(0)不能摆脱PyInt_ClearFreeList调用。

到实际问题:

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

夫妻事

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

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

之所以收集它,是因为每个进程都认为它是唯一持有对其引用的引用,严格来说,这是正确的,因为它是对字典的单个全局共享对象引用。

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

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

多重处理的行为有所不同,因为它使用os._exit而不调用退出处理程序,这显然涉及垃圾收集( 有关本主题的更多内容 )。 在脚本的裸叉版本中显式调用os._exit可获得相同的结果。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM