python中的多处理-forkserver进程从父进程继承了什么?

时间:2020-08-15 08:51:32

标签: python multiprocessing global multiprocess

我正在尝试使用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()是安全的。 没有继承不必要的资源。

也说:

比泡菜/腌制更好地继承

在使用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)

谢谢!

最好

2 个答案:

答案 0 :(得分:1)

理论

以下是Bojan Nikolic blog

的摘录

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

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

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

  3. forkserver 机制,它由一个单独的Python服务器组成,该服务器具有相对简单的状态,并且在需要新进程时由fork()处理。此方法将Fork()的速度与良好的可靠性结合在一起(因为被分叉的父对象处于简单状态)。

Forkserver

第三个方法 forkserver ,如下所示。请注意,子代保留了forkserver状态的副本。此状态旨在相对简单,但是可以通过set_forkserver_preload()方法通过多进程API进行调整。 enter image description here

实践

因此,如果您希望子进程从父进程继承simething,则必须在{strong> forkserver 状态下通过set_forkserver_preload(modules_names)来指定,它将模块名称列表设置为尝试在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

答案 1 :(得分:1)

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

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

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

当程序启动并选择forkserver start方法时,服务器进程将启动。从此以后,无论何时需要新进程,父进程都将连接到服务器并请求其派生一个新进程。 fork服务器进程是单线程的,因此使用os.fork()是安全的。没有多余的资源会被继承

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

因此,这里更值得关注。

另一方面,如果您使用fork作为启动方法,则不需要导入任何内容,因为所有子进程都将获取父进程内存的副本(如果系统使用COW,则为引用) -copy-on-write,如果我错了,请纠正我)。在这种情况下,使用global large_object将使您直接访问large_object中的worker_func

forkserver对我来说可能不是一种合适的方法,因为我面临的问题是内存开销。首先使我large_object的所有操作都消耗内存,因此我不需要工作进程中的任何不必要的资源。

如果我按照Alex的建议将所有这些计算直接放入inherited.py中,它将被执行两次 (一次是在我将模块导入main中时,一次是在服务器导入时;也许当工作者进程诞生时甚至还更多吗?),如果我只希望工作者可以从中派生出一个单线程安全进程,这将很合适。但是,由于我试图让工作人员不要继承不必要的资源,而只获得large_object,所以这将行不通。 而且将这些计算结果放在__main__的{​​{1}}中也不起作用,因为现在没有一个进程会执行它们,包括main和server。

因此,结论是,如果此处的目标是使工作人员继承最少的资源,则最好将代码分成2个,首先执行inherited.py,腌制calculation.py,然后退出解释器,然后重新开始以加载腌制的large_object。然后我可以只对large_objectfork发疯。

相关问题