最近在使用python中的multiprocessing模块时遇到一些问题,很多人应该遇到相同问题,简单研究下,供有需要的参考。
首先,要明白multiprocessing的出现很大程度是为了解决python GIL锁带来的多线程低效问题,其次,注意Windows上和Linux上的进程、线程行为不一致。
那么我们常遇到的问题如下:
1.父进程开新的子进程完成任务,父进程关闭时,必须关闭子进程
2.父进程被强制关闭时,子进程也必须关闭
3.子进程被强制关闭时,父进程也必须关闭
4.父子进程没必然联系,关闭互不影响
下面就从这四个问题出发,讨论每种问题的处理方式和在不同系统下的差别。
1.父进程关闭时,必须关闭子进程
a.常规情况
python 10.py执行如下代码
#!/usr/bin/python2.7
# -*- coding: utf-8-*-
from multiprocessing import Process
import os
import time
processes = []
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
time.sleep(30)
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.start()
processes.append(p)
time.sleep(10)
print('Parent process end.')
在Windows上和Linux上均输出
可以看到,显示Parent process end.后还需等待一段时间子进程关闭后父进程才能退出,此时ps -ux或任务管理器查看无子进程,默认父进程关闭时会等待子进程运行完毕才退出。
b.子进程设为守护进程
我们知道multiprocessing.Process有一个参数设置进程为守护进程p.daemon = True,此时子进程可以独立运行。
python 11.py执行如下代码:
#!/usr/bin/python2.7
# -*- coding: utf-8-*-
from multiprocessing import Process
import os
import time
processes = []
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
time.sleep(30)
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.daemon = True
p.start()
processes.append(p)
time.sleep(10)
print('Parent process end.')
在Windows和Linux上执行均输出:
显示Parent process end.后主进程直接退出,此时ps -ux或任务管理器查看无子进程,即父进程关闭时强制杀死了子进程。
为了取得和之前一样的效果,可以显示指定等待子进程执行完成后再退出,即指定p.join()等待子进程执行完成,代码如下,此时和之前运行效果一样。
#!/usr/bin/python2.7
# -*- coding: utf-8-*-
from multiprocessing import Process
import os
import time
processes = []
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
time.sleep(30)
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.daemon = True
p.start()
processes.append(p)
time.sleep(10)
print('Parent process end.')
for p in processes:
p.join()
c.子进程响应关闭消息
既然父进程关闭时会强制杀死守护子进程,那么不妨加上进程关闭消息响应,实现优雅关闭,执行python 12.py如下:
#!/usr/bin/python2.7
# -*- coding: utf-8-*-
from multiprocessing import Process
import os
import time
import signal
processes = []
def term(sig_num, addtion):
print '%d to term child process' % os.getpid()
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
while True:
time.sleep(10)
print('Process %s' % (os.getpid()))
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
signal.signal(signal.SIGTERM, term)
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.daemon = True
p.start()
processes.append(p)
time.sleep(10)
print('Parent process end.')
此时Windows上显示如下:
即windows还是强制关闭,父进程和子进程都关闭。
Linux显示如下:
Linux上显示Parent process end.后主进程通知子进程关闭,子进程响应了了这一消息,但是主循环仍然在持续输出,此时子进程和父进程都还存在。
这就涉及到Windows和Linux上进程结束的特点:Windows上没有一个很好的机制来通知进程关闭,这里通过TerminateProcess来强杀进程;Linux上可以给进程发送signal.SIGTERM来通知进程关闭,如果进程signal.signal注册了这个消息的响应,那么接到通知后进程如何处理退出是自己的事了,甚至可以像本示例不退出,如果不注册这个消息的响应则像上个示例一样进程被强杀。
如上逻辑,可以查看对应的源码如下:
multiprocessing.Process类中_bootstrap函数初始化运行,可以看到这里结束时都调用了util._exit_function()函数
def _bootstrap(self):
...
try:
self.run()
exitcode = 0
finally:
util._exit_function()
...
查看multiprocessing.util中util._exit_function()函数如下
...
if current_process() is not None:
for p in active_children():
if p._daemonic:
info('calling terminate() for daemon %s', p.name)
p._popen.terminate()
for p in active_children():
info('calling join() for process %s', p.name)
p.join()
...
可以看到,结束时会遍历当前进程的子进程,先对每个子进程发送terminate通知,然后等待剩余存活子进程执行完毕。多说一点,注意这里的逻辑,也就是如果子进程(包括守护进程)不全部退出,那么父进程是不退出的。
在multiprocessing.forking中查看teminate代码如下,可以看到Windows下是强制结束进程,Linux下是发送通知信号。
if sys.platform != 'win32':
...#linux
def terminate(self):
if self.returncode is None:
try:
os.kill(self.pid, signal.SIGTERM)
except OSError, e:
if self.wait(timeout=0.1) is None:
raise
else:
...#windows
def terminate(self):
if self.returncode is None:
try:
_subprocess.TerminateProcess(int(self._handle), TERMINATE)
except WindowsError:
if self.wait(timeout=0.1) is None:
raise
...
2.父进程被强制关闭时,关闭子进程
Windows中,执行python 10.py或python 11.py,任务管理器中结束父进程,查看子进程是否关闭;linux中执行python 10.py &或python 11.py &,kill 主进程pid结束父进程,ps -ux | grep python查看子进程是否关闭。
可以看到在Windows和Linux中强制关闭父进程,子进程仍然存活,被称为孤立进程。
要想主进程被强制关闭时,也关闭子进程,应该怎么做呢。前面说过Windows上结束进程没有通知机制,因此暂时没有简单办法处理;在linux上kill会给被关闭进程发送通知,既然这样,我们响应这个消息并给子进程发送关闭消息即可,代码如下:
#!/usr/bin/python2.7
# -*- coding: utf-8-*-
from multiprocessing import Process
import os
import time
import signal
processes = []
def term(sig_num, addtion):
print '%d to term child process' % os.getpid()
try:
for p in processes:
print 'process %d-%d terminate' % (os.getpid(), p.pid)
p.terminate()
# os.kill(p.pid, signal.SIGKILL)
except Exception as e:
print str(e)
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
time.sleep(100)
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.daemon = True
p.start()
processes.append(p)
signal.signal(signal.SIGTERM, term)
time.sleep(10)
print('Parent process end.')
在linux上执行python 20.py &,kill 主进程pid结束父进程,输出如下,此时ps -ux | grep python查看子进程全部关闭。可以看到代码中使用p.terminate来通知子进程关闭,前面说了这是一种友好的通知方法,如果子进程响应但是不退出也可以,如果强制子进程关闭可以直接调用os.kill(p.pid, signal.SIGKILL)发送SIGKILL消息,相当于shell中kill -9效果。注意代码中注册SIGTERM信号的位置,不能在子进程创建前注册,否则子进程也会响应term中又会对子进程发送消息,会形成死循环。
现在再说回Windows上的进程关闭,如何实现父进程关闭子进程关闭呢?显然我们需要就是一个父进程响应关闭并在实际关闭前清理子进程,这一点可以借助windows的消息机制的来处理,可在父进程中内建一个窗口接收消息,父进程响应消息后关闭子进程,使用消息通知父进程关闭即可达到和linux上一样效果,这也是在Windows上实现优雅关闭的常用方法(一般是查找主窗口发送WM_CLOSE消息)。
3.子进程被强制关闭时,关闭父进程
很自然思考子进程强制关闭时必须关闭父进程的情况,这种情况一般适用于父进程对子进程有严重依赖的地方,如父进程调试子进程等等。有了前面的探讨,很自然想到可以把父进程pid传给子进程,在通知子进程关闭时(Windows借助消息完成通知)通知父进程关闭。还有别的方法吗?
先说Linux,主进程中创建子进程时,父进程与其创建的子进程称为进程组,同一个进程组中的进程,它们的进程组ID是一致的。利用python标准库中os.getpgid方法,获取进程对应的组ID,在子进程响应关闭消息中调用os.killpg方法,向进程的组ID发送信号即可,python 30.py代码如下:
from multiprocessing import Process
import os
import time
import signal
processes = []
def term(sig_num, addtion):
print 'current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())
os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
time.sleep(50)
print('Exit child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
signal.signal(signal.SIGTERM, term)
print('Parent process %s start.' % os.getpid())
for i in xrange(0, 5):
p = Process(target=run_proc, args=('test %s' %i,))
p.daemon = True
p.start()
processes.append(p)
time.sleep(100)
print('Parent process end.')
运行结果如下:
Windows中有个作业(Job)的概念,类似进程组,可以完成相同功能;还可以使用windows的调试功能,父进程WaitForDebugEvent接收子进程的调试事件,捕获到子进程退出时,父进程也退出,这些都可以完成功能,但是必须间接调用windows系统接口了,恕不赘述。
4.父子进程没必然联系,关闭互不影响
这种情况不多,简单提下,其实multiprocessing就是对linux和windows上的接口做了包装,同时管理了父进程和子进程的关系,要想达到互不影响的目的,直接绕开multiprocessing模块调用对应系统接口接口。
参考multiprocessing源码,linux上调用os.fork(),windows上调用 _subprocess.CreateProcess即可,恕不赘述。
演示代码下载链接
原创,转载请注明来自http://blog.csdn.net/wenzhou1219