我有两个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
次调用。
对实际问题:
答案 0 :(得分:0)
情侣
在python中,多个python 进程意味着多个解释器拥有自己的GIL,GC等
d
字典不作为参数传递给进程,它是一个全局共享变量。
收集它的原因是因为每个进程认为它是唯一一个持有对它的引用的进程,严格来说,它是真的,因为它是对字典的单个全局共享对象引用。
当Python GC检查它时,它会检查该对象的ref计数器。由于只有一个共享引用,删除它意味着ref count == 0
,因此它会被收集。
要解决此问题,应将d
传递到每个分叉进程,使每个进程都拥有自己的引用。
答案 1 :(得分:0)
多处理的行为有所不同,因为它使用的os._exit
不会调用退出处理程序,这显然涉及垃圾收集(more on the topic)。在脚本的裸叉版本中明确调用os._exit
可以获得相同的结果。