多处理 fork() 与 spawn()

Posted

技术标签:

【中文标题】多处理 fork() 与 spawn()【英文标题】:multiprocessing fork() vs spawn() 【发布时间】:2021-01-13 15:56:15 【问题描述】:

我正在阅读python doc中对两者的描述:

生成

父进程启动一个新的 python 解释器进程。子进程将仅继承运行进程对象 run() 方法所需的那些资源。特别是,不会继承父进程中不必要的文件描述符和句柄。与使用 fork 或 forkserver 相比,使用这种方法启动进程相当慢。 [适用于 Unix 和 Windows。 Windows 和 macOS 上的默认设置。]

分叉

父进程使用 os.fork() 来分叉 Python 解释器。子进程在开始时实际上与父进程相同。父进程的所有资源都由子进程继承。请注意,安全地分叉多线程进程是有问题的。 [仅在 Unix 上可用。 Unix 上的默认值。]

我的问题是:

    是因为分叉更快,因为它不会尝试识别要复制的资源吗? 是不是因为 fork 复制了所有内容,与 spawn() 相比,它会“浪费”更多的资源?

【问题讨论】:

fork 由于写时复制而速度很快。 spawn 需要构建一个全新的进程 【参考方案1】:

在 3 multiprocessing start methods 之间有一个权衡:

    fork 更快,因为它会在写入时复制父进程的整个虚拟内存,包括初始化的 Python 解释器、加载的模块和内存中的构造对象。

    但是 fork 不会复制父进程的线程。因此,在父进程中由其他线程持有的锁(在内存中)被卡在子进程中,而没有拥有线程来解锁它们,当代码尝试获取它们中的任何一个时,准备好导致死锁。此外,任何具有分叉线程的本机库都将处于损坏状态。

    复制的 Python 模块和对象可能有用,或者它们可能会不必要地使每个分叉的子进程膨胀。

    子进程还“继承”操作系统资源,例如打开的文件描述符和打开的网络端口。这些也可能导致问题,但 Python 可以解决其中的一些问题。

    所以fork 速度快、不安全,而且可能臃肿。

    但是这些安全问题可能不会造成麻烦,具体取决于子进程的作用。

    spawn 从头开始​​一个 Python 子进程,没有父进程的内存、文件描述符、线程等。从技术上讲,spawn 分叉当前进程的副本,然后子进程立即调用 exec 替换自己使用新的 Python,然后要求 Python 加载目标模块并运行目标可调用对象。

    所以 spawn 是安全、紧凑且速度较慢的,因为 Python 必须加载、初始化自身、读取文件、加载和初始化模块等。

    但是与子进程所做的工作相比,它可能不会明显变慢

    forkserver 分叉当前 Python 进程的副本,将其缩减为近似于新的 Python 进程。这成为“分叉服务器”进程。然后每次启动子进程时,它都会要求 fork 服务器派生一个子进程并运行其目标可调用对象。

    这些子进程一开始都是紧凑的,没有卡住的锁。

    forkserver 更复杂,并且没有很好的文档记录。 Bojan Nikolic's blog post 解释了更多关于 forkserver 和它的秘密 set_forkserver_preload() 预加载一些模块的方法。小心使用未记录的方法,尤其是。在bug fix in Python 3.7.0 之前。

    所以 forkserver 快速、紧凑且安全,但它更复杂且没有很好的文档记录

[文档对这一切都不是很好,所以我结合了来自多个来源的信息并做出了一些推论。如有错误请发表评论。]

【讨论】:

如果我想在包括 theading.Lock 对象在内的多线程程序中使用“fork”,在主进程执行开始时创建附加进程是否是个好主意?这会使“fork”选项安全吗(例如,防止锁的“锁定时卡住”问题+假设进程在任何其他导入/指令之前创建的所有其他问题)?。 @michalmonday 如果父进程在 fork 子进程时是单线程的,那么“fork”选项会更安全。所以是的,在开始额外的线程之前,尽早分叉额外的(子)进程。我不知道“fork”有任何其他安全问题。 fork() 即使不使用模块也不会导致膨胀。这些模块占用的内存与父进程共享,因为 fork() 会进行写入时复制,因此如果子进程未使用这些模块,它们不会占用您尚未使用的任何内存。 @LieRyan 确实,如果这些页面不被使用,它们不会占用 RAM 空间,但它们会添加到子进程的地址空间,这可能会使其更接近内存不足杀手。此外,在这些页面中添加/删除对任何 Python 对象的引用将更新其引用计数,因此需要复制其页面。 Python 的循环检测 GC 可能需要扫描这些页面,从而将它们交换到 RAM 中并消耗 GC 工作。 @Jerry101 如果需要更新引用计数,那么是的,可能需要复制页面,但这仅意味着实际使用了模块。另一方面,multiprocessing spawn 方法总是制作模块的副本,无论模块是否被使用。尽管有 refcount 和 GC,但与使用 spawn 时相比,fork 需要复制的内容仍然少得多。【参考方案2】:
    是因为分叉更快,因为它不会尝试识别要复制的资源吗?

是的,它要快得多。内核可以克隆整个进程并且只复制修改过的个内存页面作为一个整体。无需将资源分配给新进程并从头启动解释器。

    是不是因为 fork 复制了所有内容,与 spawn() 相比,它会“浪费”更多的资源?

现代内核上的 Fork 只执行 "copy-on-write" 并且它只影响实际更改的内存页面。需要注意的是,“写”已经包含了仅迭代 CPython 中的对象。那是因为对象的引用计数增加了。

如果您有大量正在使用的小对象的长时间运行进程,这可能意味着您浪费的内存比使用 spawn 时更多。有趣的是,我记得 Facebook 声称通过将 Python 进程从“fork”切换为“spawn”,显着减少了内存使用量。

【讨论】:

默认是什么?产卵或分叉 @Kimi spawn:Windows,macOS 上的 Python 3.8+; fork:Unix 包括 macOS 和 Python 对于 Docker env - Python 3.8+ , Unix ,我没有使用 get_context() ,所以默认值为 None 并且它返回 self。这意味着它正在使用 Spawn ? @Crystina 后者,对。同样,您的子进程最终会获得其任务实际上不需要的页面副本,这仅仅是因为父进程使用完全不相关的对象进行了某些操作。 @Kimi 不幸的是,我不知道多处理在 Docker 中的表现如何。考虑使用Docker-tag 提出一个单独的问题。

以上是关于多处理 fork() 与 spawn()的主要内容,如果未能解决你的问题,请参考以下文章

python 多线程处理

fork 与 vfork

如何隔离 Ruby 中的方法 - 多处理问题

多处理:隐藏与 fork 进程的 DB 连接

linux C/C++多进程教程(多进程原理以及多进程的应用以多连接socket服务端为例(fork子进程处理socket_fd),同时介绍了僵尸进程产生原因与解决方法)(getpidfork)

线程基础:多任务处理(13)——Fork/Join框架(解决排序问题)