【问题标题】:multiprocessing in python - what gets inherited by forkserver process from parent process?python中的多处理-forkserver进程从父进程继承了什么?
【发布时间】:2020-12-05 00:28:08
【问题描述】:

我正在尝试使用forkserver,但在工作进程中遇到了NameError: name 'xxx' is not defined

我使用的是 Python 3.6.4,但文档应该是相同的,来自 https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods 它说:

fork 服务器进程是单线程的,因此使用 os.fork() 是安全的。 不会继承不必要的资源。

另外,它说:

比pickle/unpickle更好继承

在使用 spawn 或 forkserver 启动方法时,来自多处理的许多类型需要是可挑选的,以便子进程可以使用它们。但是,通常应该避免使用管道或队列将共享对象发送到其他进程。 相反,您应该安排程序,以便需要访问在其他地方创建的共享资源的进程可以从祖先进程继承它。

显然,我的工作进程需要处理的关键对象没有被服务器进程继承然后传递给工作人员,为什么会发生这种情况?我想知道 forkserver 进程到底从父进程继承了什么?

我的代码如下所示:

import multiprocessing
import (a bunch of other modules)

def worker_func(nameList):
    global largeObject
    for item in nameList:
        # get some info from largeObject using item as index
        # do some calculation
        return [item, info]

if __name__ == '__main__':
    result = []
    largeObject # This is my large object, it's read-only and no modification will be made to it.
    nameList # Here is a list variable that I will need to get info for each item in it from the largeObject    
    ctx_in_main = multiprocessing.get_context('forkserver')
    print('Start parallel, using forking/spawning/?:', ctx_in_main.get_context())
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=4) as pool:
        for x in pool.imap_unordered(worker_func, nameList):
            result.append(x)

谢谢!

最好的,

【问题讨论】:

  • 我尝试将 nameList 拆分为 4 个块并在 imap_unordered 中使用 zip([largeObject]*4, nameLis_splittedt) 并稍后在 worker_func() 中解开它,这样它确实得到了 @987654328 @ 进入工人,但它变得超级慢。我猜这是由于largeObject 的大小。
  • 在工作函数定义之前初始化大对象。
  • @AnmolSinghJaggi 你能解释一下如何初始化吗? largeObject 这是一个NetworkX 对象,它来自__main__ 中的一系列先前计算,其中涉及读取大熊猫df 和其他内存消耗操作。

标签: python multiprocessing global multiprocess


【解决方案1】:

理论

以下是 Bojan Nikolic blog 的摘录

现代 Python 版本(在 Linux 上)提供了三种启动单独进程的方式:

  1. Fork()-ing 父进程并继续在父进程和子进程中使用相同的进程图像。此方法速度快,但在父状态复杂时可能不可靠

  2. 生成子进程,即 fork()-ing 然后 execv 用新的 Python 进程替换进程映像。此方法可靠但速度较慢,因为进程映像会重新加载。

  3. forkserver 机制,它由一个独立的 Python 服务器组成,该服务器具有相对简单的状态,并且在需要新进程时被 fork() 处理。这种方法结合了 Fork()-ing 的速度和良好的可靠性(因为被 fork 的父级处于简单状态)。

分叉服务器

第三种方法,forkserver,如下图所示。请注意,子节点保留了 forkserver 状态的副本。这个状态本来是比较简单的,但是可以通过set_forkserver_preload()方法通过多进程API来调整这个状态。

练习

因此,如果您希望子进程从父进程继承某些东西,则必须通过 set_forkserver_preload(modules_names)forkserver 状态中指定,它设置了要尝试的模块名称列表在 forkserver 进程中加载​​。下面我举个例子:

# inherited.py
large_obj = {"one": 1, "two": 2, "three": 3}
# main.py
import multiprocessing
import os
from time import sleep

from inherited import large_obj


def worker_func(key: str):
    print(os.getpid(), id(large_obj))
    sleep(1)
    return large_obj[key]


if __name__ == '__main__':
    result = []
    ctx_in_main = multiprocessing.get_context('forkserver')
    ctx_in_main.set_forkserver_preload(['inherited'])
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=cores) as pool:
        for x in pool.imap(worker_func, ["one", "two", "three"]):
            result.append(x)
    for res in result:
        print(res)

输出:

# The PIDs are different but the address is always the same
PID=18603, obj id=139913466185024
PID=18604, obj id=139913466185024
PID=18605, obj id=139913466185024

如果我们不使用预加载

...
    ctx_in_main = multiprocessing.get_context('forkserver')
    # ctx_in_main.set_forkserver_preload(['inherited']) 
    cores = ctx_in_main.cpu_count()
...
# The PIDs are different, the addresses are different too
# (but sometimes they can coincide)
PID=19046, obj id=140011789067776
PID=19047, obj id=140011789030976
PID=19048, obj id=140011789030912

【讨论】:

  • 嗨,亚历克斯,非常感谢您的回答。所以实际上我之前读过 Bojan 的博客,当时我正试图自己解决这个问题(令人惊讶的是,内部涉及 forkserver 方法的文章并不多)。我直接试了set_forkserver_preload(large_object),自然不行:)我猜单独写一个.py就行了。
  • 但问题是,正如我在问题的 cmets 中解释的那样,large_object 这是一个 NetworkX 对象,它来自 __main__ 中的一系列先前计算,涉及读取大熊猫 df 和其他消耗内存的操作。如何修改它以像您在此处设置的 inherited.py+main.py 一样工作?
  • 我担心如果我将所有那些让我首先获得large_object 的操作包装到一个函数中并将其放入inherited.pyimport inherited 并调用该函数,当我@ 987654340@,forkserver 还会在父进程中获取不必要的文件描述符和其他东西,这将破坏我的目的,我只想让 forkserver 继承large_object
  • 尝试将这些计算移动到继承的模块中,以便在服务器导入时发生
  • 我明白你的建议......我想如果我将这些计算直接放入inherited.py,它将被执行两次(当我导入模块和另一个当服务器导入它时),如果我只想要一个工作人员可以分叉的单线程安全进程,这可能会起作用。但在这里,我试图让工人不要继承不必要的资源并且得到large_object。而且我认为将这些计算放在inherited.py 中的__main__ 中也不会起作用,因为现在没有进程会执行它们,包括主进程和服务器。
【解决方案2】:

因此,在与 Alex 进行了鼓舞人心的讨论后,我认为我有足够的信息来解决我的问题:forkserver 进程从父进程中究竟继承了什么?

基本上,当服务器进程启动时,它会导入你的主模块,if __name__ == '__main__' 之前的所有内容都会被执行。这就是为什么我的代码不起作用的原因,因为在 server 进程以及从 server 进程派生的所有工作进程中都找不到 large_object

Alex 的解决方案有效,因为 large_object 现在被导入到主进程和服务器进程中,因此从服务器派生的每个工作人员也将获得 large_object。如果与set_forkserver_preload(modules_names) 结合使用,所有工作人员甚至可能从我所看到的情况中得到相同 large_object。使用 forkserver 的原因在 Python 文档和 Bojan 的博客中有明确说明:

当程序启动并选择forkserver启动方式时,会启动一个服务器进程。从那时起,每当需要一个新进程时,父进程都会连接到服务器并请求它派生一个新进程。 fork 服务器进程是单线程的,因此使用 os.fork() 是安全的。不会继承不必要的资源

forkserver 机制,由一个独立的 Python 服务器组成,该服务器具有相对简单的状态,并且在需要新进程时使用 fork()-ed。 这种方法结合了 Fork()-ing 的速度和良好的可靠性(因为被 fork 的父级处于简单状态)

所以这里更安全。

附带说明,如果您使用fork 作为启动方法,则不需要导入任何内容,因为所有子进程都会获取父进程内存的副本(或者如果系统使用 COW-@,则为引用987654331@,如果我错了,请纠正我)。在这种情况下,使用global large_object 可以让您直接访问worker_func 中的large_object

forkserver 可能不适合我,因为我面临的问题是内存开销。首先让我获得large_object 的所有操作都占用内存,因此我不希望工作进程中有任何不必要的资源。

如果我按照 Alex 的建议将所有这些计算直接放入 inherited.py,它将被执行两次(一次在我将模块导入 main 时,一次在服务器导入它时;也许更多工作进程何时诞生?),如果我只想要一个工作人员可以分叉的单线程安全进程,这是合适的。但由于我试图让工人不要继承不必要的资源而只得到large_object,所以这行不通。 并且将这些计算放在__main__ 中的inherited.py 中也不起作用,因为现在没有进程会执行它们,包括主进程和服务器进程。

所以,作为一个结论,如果这里的目标是让工作人员继承最少的资源,我最好将我的代码分成 2,首先执行calculation.py,腌制large_object,退出解释器,然后开始一个新鲜的加载腌制的large_object。然后我就可以对forkforkserver 发疯了。

【讨论】:

    猜你喜欢
    • 2021-12-31
    • 2012-09-29
    • 1970-01-01
    • 2020-03-04
    • 2017-08-29
    • 2021-05-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-24
    相关资源
    最近更新 更多