问题描述
我正在从python doc中阅读两者的描述:
父进程将启动一个全新的python解释器进程。子进程将仅继承运行进程对象run()方法所需的那些资源。特别是,父进程中不必要的文件描述符和句柄将不会被继承。与使用fork或forkserver相比,使用此方法启动进程相当慢。 [在Unix和Windows上可用。 Windows和macOS上的默认设置。]
叉子
父进程使用os.fork()派生Python解释器。子进程开始时实际上与父进程相同。父进程的所有资源均由子进程继承。请注意,安全地分叉多线程进程是有问题的。 [仅在Unix上可用。 Unix上的默认设置。]
我的问题是:
- 分叉是否快得多,因为它不会尝试识别要复制的资源?
- 是不是因为fork复制了所有内容,与spawn()相比,它会“浪费”更多的资源?
解决方法
在 3 multiprocessing start methods 之间有一个权衡:
-
fork 速度更快,因为它对父进程的整个虚拟内存进行写时复制,包括初始化的 Python 解释器、加载的模块和内存中构造的对象。
但是 fork 不会复制父进程的线程。因此,在父进程中由其他线程持有的锁(在内存中)被卡在子进程中而没有拥有线程来解锁它们,当代码试图获取它们中的任何一个时,准备好导致死锁。此外,任何带有分叉线程的本机库都将处于损坏状态。
复制的 Python 模块和对象可能有用,或者它们可能不必要地使每个分叉的子进程膨胀。
子进程还“继承”操作系统资源,例如打开的文件描述符和打开的网络端口。这些也可能导致问题,但 Python 可以解决其中的一些问题。
所以fork 速度快、不安全,而且可能很臃肿。
但是,这些安全问题可能不会造成麻烦,具体取决于子进程的作用。
-
spawn 从头开始一个 Python 子进程,没有父进程的内存、文件描述符、线程等。从技术上讲,spawn fork 当前进程的副本,然后子进程立即调用exec 用新的 Python 替换自身,然后要求 Python 加载目标模块并运行目标可调用。
因此spawn 是安全、紧凑且速度较慢的,因为 Python 必须加载、初始化自身、读取文件、加载和初始化模块等。
但是,与子进程所做的工作相比,它可能不会明显变慢。
-
forkserver 分叉当前 Python 进程的副本,该副本将缩减为近似全新的 Python 进程。这成为“分叉服务器”过程。然后每次启动子进程时,它都会要求 fork 服务器 fork 一个子进程并运行其目标可调用。
那些子进程一开始都是紧凑的,没有卡住的锁。
forkserver 更复杂,而且没有很好的文档记录。 Bojan Nikolic's blog post 详细介绍了 forkserver 及其用于预加载某些模块的秘密
set_forkserver_preload()
方法。小心使用未记录的方法,尤其是。在 bug fix in Python 3.7.0 之前。所以forkserver 是快速、紧凑且安全的,但它更复杂且没有很好的文档记录。
[文档在所有这些方面都不是很好,所以我结合了来自多个来源的信息并做出了一些推论。对任何错误发表评论。]
,
- 分叉是否快得多,因为它不会尝试识别要复制的资源?
是的,速度更快。内核可以克隆整个过程,并且仅复制修改的内存页作为一个整体。无需将资源用于新进程并从头启动解释器。
- 是不是因为fork复制了所有内容,与spawn()相比,它会“浪费”更多的资源?
现代内核上的前叉仅执行"copy-on-write",并且仅影响实际更改的内存页面。需要注意的是,“写入”已经仅包含对CPython中的对象进行迭代。那是因为对象的引用计数增加了。
如果您的进程运行很长时间,并且正在使用许多小对象,则这可能意味着您浪费了比spawn更多的内存。有趣的是,我记得Facebook声称通过将Python进程从“ fork”切换为“ spawn”,内存使用量大大减少了。