【问题标题】:Checking fork behaviour in python multiprocessing on Linux systems在 Linux 系统上检查 python 多处理中的 fork 行为
【发布时间】:2017-04-12 22:28:33
【问题描述】:

我必须从许多进程中访问一组大型且不可挑选的 Python 对象。因此,我想确保这些对象没有被完全复制。

根据thisthis 帖子中的 cmets,除非更改对象,否则不会复制对象(在 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 都是相同的。即使列表在末尾保持不变(如预期的那样)也是如此,这意味着列表被复制。

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

【问题讨论】:

  • 一旦对象被创建,任何东西都不能改变它的ID。
  • @RossRidge:当我复制一个对象时,它不应该在内存中与原始对象不同的位置吗?有没有办法获取这个内存地址(如果id返回别的东西)?
  • 谢谢,@JonathanEunice。我试图纠正我的说法。现在还好吗?
  • 你没有复制l所指的对象,你只是修改了它。
  • @RossRidge:我并没有真正修改列表。否则,原始列表(输出的最后一行)将被更改(或者我错过了什么?)。我认为写时复制意味着当我更改对象时会进行复制。有错吗?

标签: python multiprocessing fork shared-memory


【解决方案1】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2015-04-23
    • 2015-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-29
    • 2018-07-12
    • 2021-09-05
    相关资源
    最近更新 更多