多处理:为什么 numpy 数组与子进程共享,而列表被复制?

usu*_* me 4 python fork copy-on-write python-2.7 python-multiprocessing

我使用这个脚本(见最后的代码)来评估在父进程被 fork 时是否共享或复制了全局对象。

简而言之,脚本创建一个全局data对象,子进程迭代data. 该脚本还监视内存使用情况以评估对象是否在子进程中被复制。

结果如下:

  1. data = np.ones((N,N)). 子进程中的操作: data.sum(). 结果:data共享(无复制)
  2. data = list(range(pow(10, 8))). 子进程中的操作:sum(data). 结果:data复制
  3. data = list(range(pow(10, 8))). 子进程中的操作:for x in data: pass. 结果:data复制

由于写时复制,预期结果 1)。我对结果 2) 和 3) 有点困惑。为什么会被data复制?


脚本

来源

import multiprocessing as mp
import numpy as np
import logging
import os

logger = mp.log_to_stderr(logging.WARNING)

def free_memory():
    total = 0
    with open('/proc/meminfo', 'r') as f:
        for line in f:
            line = line.strip()
            if any(line.startswith(field) for field in ('MemFree', 'Buffers', 'Cached')):
                field, amount, unit = line.split()
                amount = int(amount)
                if unit != 'kB':
                    raise ValueError(
                        'Unknown unit {u!r} in /proc/meminfo'.format(u = unit))
                total += amount
    return total

def worker(i):
    x = data.sum()    # Exercise access to data
    logger.warn('Free memory: {m}'.format(m = free_memory()))

def main():
    procs = [mp.Process(target = worker, args = (i, )) for i in range(4)]
    for proc in procs:
        proc.start()
    for proc in procs:
        proc.join()

logger.warn('Initial free: {m}'.format(m = free_memory()))
N = 15000
data = np.ones((N,N))
logger.warn('After allocating data: {m}'.format(m = free_memory()))

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

详细结果

运行 1 个输出

[WARNING/MainProcess] Initial free: 25.1 GB [WARNING/MainProcess] After allocating data: 23.3 GB [WARNING/Process-2] Free memory: 23.3 GB [WARNING/Process-4] Free memory: 23.3 GB [WARNING/Process-1] Free memory: 23.3 GB [WARNING/Process-3] Free memory: 23.3 GB

运行 2 输出

[WARNING/MainProcess] Initial free: 25.1 GB [WARNING/MainProcess] After allocating data: 21.9 GB [WARNING/Process-2] Free memory: 12.6 GB [WARNING/Process-4] Free memory: 12.7 GB [WARNING/Process-1] Free memory: 16.3 GB [WARNING/Process-3] Free memory: 17.1 GB

运行 3 输出

[WARNING/MainProcess] Initial free: 25.1 GB [WARNING/MainProcess] After allocating data: 21.9 GB [WARNING/Process-2] Free memory: 12.6 GB [WARNING/Process-4] Free memory: 13.1 GB [WARNING/Process-1] Free memory: 14.6 GB [WARNING/Process-3] Free memory: 19.3 GB

Tim*_*ers 5

它们都是写时复制的。你缺少的是当你这样做时,例如,

for x in data:
    pass
Run Code Online (Sandbox Code Playgroud)

包含在其中的每个对象的引用计数data临时增加 1,一次一个,因为x依次绑定到每个对象。对于int对象,CPython 中的引用计数是基本对象布局的一部分,因此对象会被复制(您确实对其进行了变异,因为引用计数发生了变化)。

为了使事情更类似于这种numpy.ones情况,请尝试,例如,

data = [1] * 10**8
Run Code Online (Sandbox Code Playgroud)

然后只有一个唯一的对象10**8被列表引用了很多次( ) ,所以几乎不需要复制(同一个对象的引用计数被多次增加和减少)。

  • COW(写时复制)是一个操作系统概念,Python 在支持该功能的平台上从`fork()` 继承。COW 的设计没有考虑到 Python 的多处理 (mp),而 Python 的 mp 也没有考虑到 COW ;-) COW 是特定于平台的东西,它可能有用也可能没有帮助,具体取决于应用程序。请注意,对于 CPython 中的许多对象类型,引用计数 _not_ 与“大量对象数据”一起存储。无论如何,COW 最初是为了实现 exec-after-fork 而发明的,其优点是从不引用大量的父进程数据。 (2认同)