在Linux系统上检查python多处理中的fork行为

时间:2016-11-29 03:18:05

标签: python multiprocessing fork shared-memory

我必须从许多进程访问一组大而不可拾取的python对象。因此,我想确保不会完全复制这些对象。

根据thisthis帖子中的评论,除非更改了对象,否则不会复制对象(在unix系统上)。但是,引用对象将更改其引用计数,然后将复制该引用计数。

到目前为止这是正确的吗?由于我的关注是由于我的大对象的大小,我没有问题,如果这些对象的小部分被复制。

为了确保我正确理解所有内容并且没有意外发生,我实施了一个小型测试程序:

from multiprocessing import Pool

def f(arg):
    print(l, id(l), object.__repr__(l))
    l[arg] = -1
    print(l, id(l), object.__repr__(l))

def test(n):
    global l
    l = list(range(n))
    with Pool() as pool: 
        pool.map(f, range(n))
    print(l, id(l), object.__repr__(l))

if __name__ == '__main__':
    test(5) 

f的第一行,我希望id(l)在所有函数调用中返回相同的数字,因为在id检查之前列表不会更改。

另一方面,在f的第三行中,id(l)应在每个方法调用中返回不同的数字,因为列表在第二行中更改。

但是,程序输出让我很困惑。

[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[-1, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, -1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, -1, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, -1, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, -1] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488

f的所有通话和行中的ID都相同。即使列表在结尾处保持不变(如预期的那样),也就是这种情况,这意味着列表已经被复制。

如何查看对象是否已被复制?

1 个答案:

答案 0 :(得分:4)

您的困惑似乎是由于误解了流程和fork的工作原因造成的。每个进程都有自己的地址空间,因此两个进程可以使用相同的地址而不会发生冲突。这也意味着进程无法访问另一个进程的内存,除非将相同的内存映射到两个进程。

当进程调用fork系统调用时,操作系统会创建一个新的子进程,该进程是父进程的克隆。与任何其他进程一样,此克隆具有与其父进程不同的自己的地址空间。但是,地址空间的内容是父母的精确副本。这通常是通过将父进程的内存复制到为子进程分配的新内存中来完成的。这意味着一旦孩子和父母在fork任何修改之后恢复执行,任何一个过程对他们自己的记忆都没有影响另一个。

然而,复制过程的整个地址空间是一项昂贵的操作,并且通常是浪费。大多数情况下,新进程立即执行新程序,导致子地址空间被完全替换。因此,现代类Unix操作系统使用&#34; copy-on-write&#34; fork实施。不是复制父进程的内存,而是将父进程的内存映射到子进程,以便它们可以共享相同的内存。但是,旧的语义仍然保持不变。如果子级或父级修改共享内存,则复制的页面将被复制,以便这两个进程不再共享该页面的内存。

multiprocessing模块调用您的f函数时,它会在使用fork系统调用创建的子进程中执行此操作。由于此子进程是父进程的克隆,因此它还有一个名为l的全局变量,该变量引用在两个进程中具有相同ID(地址)和相同内容的列表。也就是说,直到您修改子进程中l引用的列表。 ID不会(并且不能)更改,但列表的子版本不再与父级相同。父母列表的内容不受孩子的修改影响。

请注意,前一段中描述的行为是否为fork是否使用写时复制。至于multiprocessing模块和Python一般都只关注实现细节。无论如何,有效结果是一样的。这意味着您无法在使用fork实现的Python程序中进行测试。