在普通的Python代码中,我可以理解进程的生命周期。例如执行
python script.py
时:
python script.py
,os创建一个新进程开始执行python
.python
可执行文件设置解释器,并开始执行script.py
。script.py
执行完成时,python解释器将退出。在多处理的情况下,我很好奇其他进程会发生什么。
以下面的代码为例:
# test.py
import multiprocessing
# Function to compute square of a number
def compute_square(number):
print(f'The square of {number} is {number * number}')
if __name__ == '__main__':
# List of numbers
numbers = [1, 2, 3, 4, 5]
# Create a list to keep all processes
processes = []
# Create a process for each number to compute its square
for number in numbers:
process = multiprocessing.Process(target=compute_square, args=(number,))
processes.append(process)
process.start()
# Ensure all processes have finished execution
for process in processes:
process.join()
print("All processes have finished execution.")
当我执行
python test.py
时,我知道 test.py
将作为 __main__
模块执行。但是其他进程会发生什么情况呢?
具体来说,当我执行
multiprocessing.Process(target=compute_square, args=(number,)).start()
时,该过程会发生什么?
该进程如何调用Python解释器?如果它只是
python script.py
,它如何知道需要执行名为 compute_square
的函数?或者它使用 python -i
,并通过管道传递命令来执行?
根据 multiprocessing
模块的
Python 文档,用于创建进程的底层系统功能取决于平台,具有 3 种不同的“启动方法”:
spawn
、fork
和 forkserver
.
默认使用哪一个取决于平台,尽管您可以使用
multiprocessing.set_start_method()
自行选择启动方法,并将方法名称作为字符串。
当您在 POSIX 系统上使用
fork()
时,子进程几乎是其父进程的克隆 – 当然除了它的 PID 和其他必要的差异。它从内存中的同一点运行相同的代码,虽然它们的内存页面最初是共享的,但在写入任何共享页面时,会为每个进程创建一个专用副本。在我看来,这是最容易理解的模型,只需将新进程视为初始进程的完整副本,除了它知道它是子进程(而不是父进程),并决定运行结果请求的功能。
如果您不熟悉
fork()
,我鼓励您阅读它,也许从 维基百科文章开始,然后是 手册页。
spawn
还有一篇 维基百科文章 和 手册页。
对于 Python,我们可以使用一个简单的程序来测试所有 3 个启动方法,该程序将
spawn
、fork
或 forkserver
作为第一个参数:
from multiprocessing import Process, set_start_method
import os
import random
import sys
# generate a random value in the parent process
r = random.randint(0, int(1e9))
def info(msg):
print(f'pid: {os.getpid()}, ppid: {os.getppid()}, module: {__name__}, msg: {msg}')
def f(arg):
info(f'[child] arg: {arg}, r: {r}')
if __name__ == '__main__':
if len(sys.argv) != 2:
print(f'Usage: {sys.argv[0]} spawn|fork|forkserver')
sys.exit(1)
method = sys.argv[1]
print(f'setting start method: {method}')
set_start_method(method)
info(f'[parent] r: {r}')
p = Process(target=f, args=('hello',))
p.start()
p.join()
当使用
spawn
时,将创建一个新的解释器并调用进程的入口点函数。看看随机变量r
是如何不被保留的:
setting start method: spawn
pid: 44658, ppid: 1356, module: __main__, msg: [parent] r: 242489315
pid: 44688, ppid: 44658, module: __mp_main__, msg: [child] arg: hello, r: 229487814
使用
fork
,我们会在调用 p.start()
的位置克隆进程,因此我们仍然会看到 r
的相同值:
setting start method: fork
pid: 49721, ppid: 1356, module: __main__, msg: [parent] r: 376097656
pid: 49738, ppid: 49721, module: __main__, msg: [child] arg: hello, r: 376097656
对于
forkserver
,我本希望多个进程继承相同的r
值,但情况似乎并非如此:
setting start method: forkserver
pid: 66317, ppid: 1356, module: __main__, msg: [parent] r: 917735863
pid: 66336, ppid: 66334, module: __mp_main__, msg: [child] arg: hello, r: 698876823
在第一个进程之后启动和停止第二个进程不会为其提供与根进程或第一个子进程相同的
r
值,这表明子进程派生的服务器进程甚至可能已经启动早于脚本开始。