一、简介
有三种多进程启动方法之间存在权衡:
fork 更快,因为它进行的是父进程的整个虚拟内存的写时复制,这包括已初始化的Python解释器、已加载的模块和内存中的构造对象。
但是,fork 不会复制父进程的线程。因此,父进程中由其他线程持有的锁(在内存中)会被卡在子进程中,没有拥有能解锁它们的线程,可能会在试图获取这些锁时造成死锁。此外,带有fork线程的任何本地库都将处于损坏状态。
复制的Python模块和对象可能有用,也可能会使每个派生的子进程膨胀不必要。
子进程还会“继承”操作系统资源,如打开的文件描述符和开放的网络端口。这些也可能导致问题,但Python对其中一些问题进行了处理。
所以fork快,但不安全,也可能会膨胀。
然而,这些安全问题可能不会带来问题,这取决于子进程做什么。
spawn 从头开始启动一个Python子进程,不使用父进程的内存、文件描述符、线程等。从技术上讲,spawn会分叉当前进程的一个复制,然后子进程立即调用exec替换自己为新的Python,要求Python加载目标模块并运行目标可调用对象。
因此,spawn是安全的、紧凑的,但速度较慢,因为Python必须加载、初始化自己,读取文件,加载和初始化模块等。
但与子进程执行的任务相比,它可能不会明显延迟。
forkserver 分叉了一个复制当前Python进程,大致上变成一个新的Python进程。这变成“fork server”进程。每当启动一个子进程时,它会要求fork server分叉一个子进程并运行其目标可调用对象。
这些子进程都以紧凑的方式启动,且没有卡住的锁。
forkserver较为复杂,文档也不是很好。Bojan Nikolic的博客文章更多地解释了forkserver及其秘密方法来预加载某些模块。要小心使用未记录的方法,尤其是在Python 3.7.0.set_forkserver_preload()漏洞修复之前。
因此,forkserver快速、紧凑且安全,但它更为复杂,文档不够充分。
[参考文献的说明不够充分,所以我结合了多个来源的信息并进行了一些推断。请在评论区指出任何错误。]
二、总结:
Python有三种启动多进程的方法:
- fork启动方法:可以更快地进行写时复制,复制此进程的整个内存。由于速度更快,可以在需要派生出大量进程或快速派生进程的情况下使用。但是,由于fork不复制parent中的线程,因此在出现锁竞争的情况下,子进程可能会继承不必要的线程等其他不必要的开销。此外,由于子进程会继承父进程中的所有资源,包括文件描述符和网络接口,因此在特定情况下可能会带来一些问题。因此,使用fork的时候需要谨慎。
- spawn启动方法:在任何Python版本中都可以使用,它启动了一个干净的新进程来运行Python代码。使用spawn方法的好处在于从父进程中不继承任何资源。目标Python进程从头开始启动,并具有与父进程孤立的空间。 这样可以避免死锁等问题,但是启动时间相对较长,因为处理器必须加载、初始化Python解释器本身,并从磁盘读取模块以及其他资源等。因此,如果需要启动一些子进程并且需要充分利用系统资源,可以使用spawn。
- forkserver启动方法:使用较新的Python版本,可以避免父进程中的锁同步问题和开销。 当从父进程中启动一个子进程时,将从创建的“fork server”进程中获取一个子进程,并在派生子进程之间重用。 因为“fork server”是干净的,没有绑定到特定工作负载,所以可以避免在从父进程派生后不同进程之间共享的问题。使用该方法的优点是可以在处理器上派生许多子进程,且运行时间也相对较快。
三、例子
multiprocessing.set_start_method('spawn')
用于设置使用spawn
启动方法创建子进程,在此方法中,子进程启动得到干净的执行环境。它应该在程序的最开始被调用,因为只有在程序开始运行时才能启动子进程。下面是一个示例:
import multiprocessing
def worker():
print('Worker')
if __name__ == '__main__':
multiprocessing.set_start_method('spawn')
p = multiprocessing.Process(target=worker)
p.start()
p.join()
在上面的示例中,set_start_method('spawn')
被调用来设置使用spawn
启动方法。接着,一个新的子进程被创建,并在子进程中执行worker
函数。使用join
等待子进程完成并退出。注意,必须在__name__ == '__main__'
下运行不然可能会发生错误。