通过fork在多处理中收集共享数据的垃圾

garbage collection of shared data in multiprocessing via fork

我正在 linux 中进行一些多处理,我正在使用当前未显式传递给 child 进程(不是通过参数)的共享内存。

在官方 python 多处理 Programming guidelines 的 "Explicitly pass resources to child processes" 部分是这样写的:

On Unix using the fork start method, a child process can make use of a shared resource created in a parent process using a global resource. However, it is better to pass the object as an argument to the constructor for the child process.... this ... ensures that as long as the child process is still alive the object will not be garbage collected in the parent process. This might be important if some resource is freed when the object is garbage collected in the parent process.

这个解释我觉得有点欠缺。

  1. 我什么时候应该担心垃圾收集?
  2. 我是否应该始终将数据传递给 child,否则有时会出现意外结果,或者这只是最佳做法吗?

现在我没有遇到任何意外的垃圾收集,但是这种情况对我来说似乎不稳定。

这在很大程度上取决于 A) 您的数据 B) 您的多进程 method.

TLDR:

  • spawn object 被克隆并且每个都被最终确定 在每个进程中
  • fork/forkserver object在主进程共享完成

  • 一些 object 对在主进程中完成但仍在 child 进程中使用的响应很差。

  • args 上的文档是错误的,因为 args 的内容不会自行保存 (3.7.0)

注:Full code available as gist。 macOS 10.13.

上 CPython 3.7.0 的所有输出

我们从一个简单的 object 开始,报告最终确定的时间和地点:

def print_pid(*args, **kwargs):  # Process aware print helper
    print('[%s]' % os.getpid(), *args, **kwargs)


class Finalisable:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return '<Finalisable object %s at 0x%x>' % (getattr(self, 'name', 'unknown'), id(self))

    def __del__(self):
        print_pid('finalising', self)

早 collection 来自 args

为了测试 args 对 GC 的工作原理,我们可以构建一个进程并立即释放其参数引用:

def drop_early():
    payload = Finalisable()
    child = multiprocessing.Process(target=print, args=(payload,))
    print('drop')
    del payload  # remove sole local reference for `args` content
    print('start')
    child.start()
    child.join()

使用 spawn 方法,收集了原件,但 child 有自己的副本要完成:

### test drop_early in 15333 method: spawn
drop
start
[15333] finalising <Finalisable object early at 0x102347390>
[15336] child sees <Finalisable object early at 0x109bd8128>
[15336] finalising <Finalisable object early at 0x109bd8128>
### done

fork方法,原件定型,child收到这个定型object:

### test drop_early in 15329 method: fork
drop
start
[15329] finalising <Finalisable object early at 0x108b453c8>
[15331] child sees <Finalisable object early at 0x108b453c8>
### done

这表明主进程的有效载荷在 child 进程运行并完成之前完成! 归根结底,args不是防早collection!

早 collection 共享 objects

Python 有 some types meant for safe sharing between processes。我们也可以使用它作为我们的标记:

def drop_early_shared():
    payload = Finalisable(multiprocessing.Value('i', 65))
    child = multiprocessing.Process(target=print_pid, args=('child sees', payload,))
    print('drop')
    del payload
    print('start')
    child.start()
    child.join()

使用 fork 方法,Value 被提早收集但仍然有效:

### test drop_early_shared in 15516 method: fork
drop
start
[15516] finalising <Finalisable object <Synchronized wrapper for c_int(65)> at 0x1071a3e10>
[15519] child sees <Finalisable object <Synchronized wrapper for c_int(65)> at 0x1071a3e10>
### done

使用 spawn 方法,Value 被提早收集并且完全破坏了 child:

### test drop_early_shared in 15520 method: spawn
drop
start
[15520] finalising <Finalisable object <Synchronized wrapper for c_int(65)> at 0x103a16c18>
[15524] finalising <Finalisable object unknown at 0x101aa0128>
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/spawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/spawn.py", line 115, in _main
    self = reduction.pickle.load(from_parent)
  File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/synchronize.py", line 111, in __setstate__
    self._semlock = _multiprocessing.SemLock._rebuild(*state)
FileNotFoundError: [Errno 2] No such file or directory
### done

这表明最终化行为取决于您的 object 和您的环境。 底线,不要假设你的 object 是 well-behaved!


虽然通过 args 传递数据是一种很好的做法,但这 不会 使主进程免于处理它! Objects 当主进程删除引用时,可能会对提前完成做出糟糕的响应。

由于 CPython 使用 fast-acting 引用计数,您几乎可以立即看到不良影响。然而,其他实现,例如PyPy,可以在任意时间隐藏这样的 side-effects。