Python的多进程模块中的.join()方法到底是做什么的?

155

了解Python的Multiprocessing(来自PMOTW文章),希望能对join()方法是做什么的进行一些澄清。

一篇2008年的旧教程中,它指出,如果代码中没有p.join()调用,则“子进程将闲置不动,不会终止,变成一个必须手动结束的僵尸进程”。

from multiprocessing import Process

def say_hello(name='world'):
    print "Hello, %s" % name

p = Process(target=say_hello)
p.start()
p.join()

我加入了一个PID的打印以及一个time.sleep来测试,就我所知,这个进程是自动终止的:

from multiprocessing import Process
import sys
import time

def say_hello(name='world'):
    print "Hello, %s" % name
    print 'Starting:', p.name, p.pid
    sys.stdout.flush()
    print 'Exiting :', p.name, p.pid
    sys.stdout.flush()
    time.sleep(20)

p = Process(target=say_hello)
p.start()
# no p.join()

20秒之内:

936 ttys000    0:00.05 /Library/Frameworks/Python.framework/Versions/2.7/Reso
938 ttys000    0:00.00 /Library/Frameworks/Python.framework/Versions/2.7/Reso
947 ttys001    0:00.13 -bash

20秒后:

947 ttys001    0:00.13 -bash

加回文件结尾处的p.join()后,行为与之前相同。Python Module of the Week提供了一个非常易懂的模块说明:“使用join()方法等待进程完成其工作并退出”,但似乎至少在OS X上已经这样做了。

我也在想这个方法的名称。这里的.join()方法是否在连接任何内容?它是将进程与其结束连接起来吗?还是只是与Python本机的.join()方法共享名称?


4
据我所知,它持有主线程并等待子进程完成,然后在主线程中重新加入资源,通常会进行干净的退出。 - abhishekgarg
啊,这有道理。所以实际上,“CPU、内存资源”是从父进程中分离出来的,然后在子进程完成之后再“合并”回来? - MikeiLL
1
是的,这就是它正在执行的操作。因此,如果您不将它们重新连接起来,当子进程完成时,它就会变成僵尸进程或死进程。 - abhishekgarg
@abhishekgarg 这不是真的。当主进程完成时,子进程将被隐式加入。 - dano
@abhishekgarg 是的,一旦它们完成,子进程将会显示为僵尸进程,直到主进程退出(或显式调用join())。 - dano
显示剩余3条评论
6个回答

168
join()方法在使用threadingmultiprocessing时与str.join()没有关系,它实际上并不将任何东西连接在一起。相反,它只是意味着“等待此[线程/进程]完成”。之所以使用名称join,是因为multiprocessing模块的API旨在看起来与threading模块的API尽可能相似,而threading模块在其Thread对象中使用join。在许多编程语言中,使用术语join表示“等待线程完成”很常见,因此Python也采用了这个术语。
现在,你无论是否调用 join(),都会看到20秒的延迟,这是因为默认情况下,当主进程准备退出时,它会隐式地在所有正在运行的 multiprocessing.Process 实例上调用 join()。这个在 multiprocessing 文档中没有被明确说明,但在 编程指南 部分提到了:

还要记住非守护进程将自动加入。

你可以通过在启动进程之前将 Process 上的 daemon 标志设置为 True 来覆盖此行为:
p = Process(target=say_hello)
p.daemon = True
p.start()
# Both parent and child will exit here, since the main process has completed.

如果你这样做,子进程将会在主进程完成后立即终止:

守护进程

该进程的守护标志,一个布尔值。在调用 start() 前必须设置。

初始值从创建进程继承而来。

当一个进程退出时,它会尝试终止所有它的守护子进程。


7
我理解p.daemon=True是为了“启动一个后台进程,使其在不阻塞主程序退出的情况下运行”。但是如果“守护进程在主程序退出之前会被自动终止”,那么它到底有什么用呢? - MikeiLL
9
@MikeiLL: 基本上,任何您想要在父进程运行时在后台执行的任务都可以,但不需要在退出主程序之前优雅地清理。也许是一个工作进程,从套接字或硬件设备读取数据,并通过队列将数据反馈给父进程,或者为某个目的在后台处理数据?总的来说,我认为使用 'daemonic' 子进程并不安全,因为该进程将被终止而无法清理可能打开的任何资源。 - dano
7
更好的做法是在主进程退出之前,向子进程发出清理和退出的信号。你可能认为在父进程退出时保留后台子进程运行很有道理,但请记住,multiprocessing API 的设计目标是尽可能模拟threading API 。当主线程退出时,后台的 threading.Thread 对象会立即终止,因此后台的 multiprocesing.Process 对象也会表现出相同的行为。 - dano

50

没有使用join(),主进程可能会在子进程之前完成。我不确定这种情况下是否会导致僵尸进程。

join()的主要目的是确保在主进程依赖于子进程的工作之前,子进程已经完成。

join()的词源是它是fork的相反,fork是Unix类操作系统中创建子进程的常用术语。一个进程“fork”成为几个进程,然后“join”回到一个进程。


2
它使用名称 join(),因为join()是用于等待threading.Thread对象完成的方法,而multiprocessing API旨在尽可能模仿threading API。 - dano
你的第二个陈述解决了我目前项目中正在处理的问题。 - MikeiLL
我理解主线程等待子进程完成的部分,但这不是异步执行的目的吗?难道不应该独立地完成执行(子任务或进程)吗? - Apurva Kunkulol
1
@ApurvaKunkulol 取决于你如何使用它,但在主线程需要子线程工作的结果时需要使用 join()。例如,如果你正在渲染某些内容,并将最终图像的1/4分配给4个子进程之一,并希望在完成后显示整个图像。 - Russell Borogove
@RussellBorogove 啊!我明白了。那么异步活动的意义在这里有点不同。它必须仅表示子进程旨在与主线程同时执行其任务,而主线程也在执行其工作,而不是只是闲等子进程。 - Apurva Kunkulol
在我看来,这不是一个非常有用的区分。 - Russell Borogove

20

我不会详细解释 join 的作用,但这是它的词源和直觉,这将帮助您更容易地记住其含义。

思想是执行“分叉”成多个进程,其中一个是主/主要进程,其余是工作者(或辅助/次要)。当工作者完成后,它们“加入”主进程,以便可以恢复串行执行。

join()导致主进程等待工作者加入它。该方法可能更好地被称为“等待”,因为这是它在主机器上实际引起的行为(尽管POSIX中被称为“join”,但POSIX线程也称之为“join”)。加入仅作为线程适当协作的结果发生,而不是主要进程所做的事情。

自1963年以来,这个含义的名称“fork”和“join”已经在多进程中使用


因此,在某种程度上,单词“join”的这种用法可能先于它指代连接而非相反的拼接。 - MikeiLL
1
这种连接用法不太可能源于多进程使用中的连接用法;相反,这两个意义都是从单词的普通英语意义中分别派生出来的。 - Russell Borogove

9
join() 方法确保所有子进程执行完毕后再执行代码的后续部分。
例如,在没有使用 join() 的情况下,以下代码会在进程结束之前调用 restart_program(),类似于异步操作、不是我们所期望的(可以尝试一下):
num_processes = 5

for i in range(num_processes):
    p = multiprocessing.Process(target=calculate_stuff, args=(i,))
    p.start()
    processes.append(p)
for p in processes:
    p.join() # call to ensure subsequent line (e.g. restart_program) 
             # is not called until all processes finish

restart_program()

3

join() 用于等待工作进程退出。在使用 join() 前,必须先调用 close()terminate()

像 @Russell 所提到的,join 类似于 fork(生成子进程)的反向操作。

要运行 join,您必须运行 close(),这将防止提交更多任务到池中,并在所有任务完成后退出。或者,运行 terminate() 将立即停止所有工作进程退出。

"子进程将闲置不动,无法终止,成为您必须手动杀死的僵尸" 这种情况可能发生在主(父)进程退出但子进程仍然运行并且完成后没有父进程返回其退出状态。


2

要等待进程完成其工作并退出,使用join()方法。

注意:在终止进程后使用join()将进程加入,以便让后台机制有足够的时间来更新对象的状态以反映终止。

这是一个很好的例子,可以帮助我理解:此处

个人注意到的一件事是,使用join()方法使得主进程暂停,直到子进程完成其进程,这就打破了我一开始使用multiprocessing.Process()的初衷。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接