[Python学习日记-85] 并发编程之多进程 ------ Process 类、join 方法、僵尸进程与孤儿进程
[multiprocessing 模块](#multiprocessing 模块)
[Process 类](#Process 类)
简介
在前面的进程理论的介绍当中我们已经介绍了进程的概念、并发与并行的区别以及进程并发的实现理论,这些都是比较偏向于理论的。本篇我们将进入实际敲代码的阶段,看看在 Python 当中应该如何创建进程,以及使用进程时有什么需要注意的地方。
multiprocessing 模块
现在这个时代基本上已经很少看到单核的 CPU 了,基本上都是多核的 CPU,而在 Python 中的多线程并无法利用多核的优势,只能一个核分时的去多线程操作,如果想要充分的利用多核的资源,这个时候就是 multiprocessing 模块登场的时候了。
multiprocessing 模块是 Python 中用于支持多进程编程的标准库,该模块与多线程模块 threading 的编程接口类似。我们可以利用该 multiprocessing 模块来开启子进程,并在子进程中执行我们定制的任务(函数之类的)。
multiprocessing 模块支持子进程的创建;提供了在多个进程之间进行通信、协同和共享数据的工具;使得可以利用多核处理器来同时执行多个任务,即可以执行不同形式的同步形式,从而可以更方便地进行并行编程,充分利用计算资源,提高程序的效率和性能。
此外还提供了 Process、Pool、Queue、Pipe、Lock 等组件。
- Process 类:用于表示一个进程,可以通过创建该类的实例来启动一个新的进程。
- Pool 类:提供了一种将任务分配给多个进程来并行执行的方式。
- Queue 类:用于在多个进程之间传递数据,实现进程间的通信。
- Pipe 函数:用于在两个进程之间创建一个管道,实现进程间的通信。
- Lock 类:提供了一个简单的锁对象,用于在多个进程之间同步访问共享资源。
Process 类
multiprocessing 模块提供了用于创建并管理进程的类和函数。其中,Process 类是用于创建进程的,并且可以在实例化 Process 类时创建进程并把将要执行的任务分配给这个进程(此时任务尚未执行),Process 类的结构如下所示
Process([group [, target [, name [, args [, kwargs]]]]])
参数介绍:
- group:进程组,若未使用则始终为 None
- target:要在子进程中运行的函数,即子进程要执行的任务
- name:子进程的名称
- args:传递给 target 指向函数的位置参数,以元组的形式传入,例如,args=(1,2,'jove')
- kwargs:传递给 target 指向函数的关键参数,以字典的形式传入,例如,kwargs=('name':'jove','age':18)
注意:
1、实例化时需要使用关键字的方式来指定参数
2、args 是一个元组,传入的参数必须有逗号,即使只传一个参数(例如,('子进程1',))
3、args 传入的参数是 target 指定函数的位置参数
方法介绍:
- p.start():启动子进程,该方法会调用子进程的 p.run() 方法来执行 target 指向的函数或可调用对象
- p.run():该方法定义了子进程的行为,target 指向函数的调用正是由该方法来进行的,如果我们使用自定义类的方式实现多进程,则一定要在自定义类中实现该方法
- p.terminate():强制终止子进程 p,该方法不会进行任何清理操作,如果 p 创建了子进程,该子进程就变成了僵尸进程,使用该方法需要注意这种情况。如果 p 还保存了一个锁那么也不会被释放,从而导致死锁现象的发生
- p.is_alive():返回一个布尔值,表示子进程 p 是否仍在运行,如果 p 仍然运行将返回 True
- p.join([timeout]):阻塞主进程(只能阻塞使用 start() 方法开启的进程),等待子进程 p 的终止或超时,即主线程处于等待状态,而子进程 p 是处于运行状态。timeout 参数(可选)指定最长等待时间
- p.close():关闭底层资源,释放进程资源
属性介绍:
- p.daemon:守护进程标志,可以通过赋值操作把进程标志为守护进程,需要在 p.start() 之前进行设置。如果设置为 True,则子进程 p 会随着父进程的结束而结束,并且 子进程 p 无法创建自己的新进程;如果设置为 False,则子进程会继续运行,直到完成任务或被手动终止
- p.name:返回进程的名称,也可以通过赋值操作来修改进程名称
- p.pid:返回子进程的进程ID(Process ID)
- p.exitcode:返回进程的退出码。如果进程尚未结束,则返回 None,如果为 -N 则表示被信号 N 结束了进程。
- p.authkey:返回进程的身份验证密钥,用于验证进程之间的通信,默认是由 os.urandom() 随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功
- p.sentinel:确认子进程是否为 Sentinel 进程(Sentinel 进程是一个特殊的子进程,在主进程退出时会自动终止所有子进程),是则返回 True
一**、** 开启进程的两种方式
方法一:实例化 Process 类来开启进程
python
from multiprocessing import Process
import time
def task(name):
print('%s is running' % name)
time.sleep(3)
print('%s is done' % name)
if __name__ == '__main__':
p1 = Process(target=task,args=('子进程1',)) # 等价于 Process(target=task,kwargs={'name': '子进程1'})
p2 = Process(target=task, args=('子进程2',))
p3 = Process(target=task, args=('子进程3',))
p1.start() # 仅仅只是给操作系统发送一个信号,从下面的输出可以看出"主进程"的打印是快于p1,p2,p3子进程的
p2.start()
p3.start()
print('主进程')

方法二:自定义类继承 Process 类来开启进程
python
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
print('%s is running' % self.name)
time.sleep(3)
print('%s is done' % self.name)
if __name__ == '__main__':
p1 = MyProcess('子进程1')
p2 = MyProcess('子进程2')
p3 = MyProcess('子进程3')
p1.start() # 仅仅只是给操作系统发送一个信号,从下面的输出可以看出"主进程"的打印是快于p1,p2,p3子进程的
p2.start()
p3.start()
print('主进程')

注意:
1、在 Windows 中 Process() 的调用必须放到: 下,原因如下
由于 Windows 没有 fork,多处理模块启动一个新的 Python 进程并导入调用模块。
如果在导入时调用 Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。
这是隐藏对 Process() 内部调用的原因,当使用 if name == "main" 时,这个 if 语句中的语句将不会在导入时被调用。
2、进程之间的内存空间是相互隔离的
python
from multiprocessing import Process
import time
n=100 # 全局变量,Windows中要把该变量放到 if __name__ == '__main__': 的前面
def work():
global n
n=0
print('子进程内: ',n)
if __name__ == '__main__':
p=Process(target=work)
p.start()
time.sleep(2) # 如果正常运行主进程会先打印,从输出看不出内存空间相互隔离
print('主进程内: ',n)

二、join 方法
在主进程运行过程中如果想并发地执行其他的任务,我们可以开启子进程,此时主进程的任务与子进程的任务分两种情况:
- 情况一:在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源
- 情况二:如果主进程的任务在执行到某一个阶段时,需要等待子进程执行完毕后才能继续执行,就需要有一种机制能够让主进程检测子进程是否运行完毕,在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是 join() 的作用
python
from multiprocessing import Process
import time
import random
import os
def task(name):
print('%s is running' % name,os.getpid())
time.sleep(random.randrange(1,3))
print('%s is done' % name,os.getpid())
if __name__ == '__main__':
p = Process(target=task,args=('子进程1',))
p.start()
p.join(0.0001) # 等待 p 停止,只等了0.0001秒就继续干自己的活了
print('主进程')

那使用了 join() 之后程序变成了串行吗?
并不是的,我们要搞清楚 join() 是让那个进程做了等待,join() 是让主进程等待,并不是让子进程 p 进行等待,一下面的代码为例
python
from multiprocessing import Process
import time
import random
import os
def task(name):
print('%s is running' % name,os.getpid())
time.sleep(random.randrange(1,3))
print('%s is done' % name,os.getpid())
if __name__ == '__main__':
p1 = Process(target=task,args=('子进程1',))
p2 = Process(target=task, args=('子进程2',))
p3 = Process(target=task, args=('子进程3',))
p4 = Process(target=task, args=('子进程4',))
p_list = [p1,p2,p3,p4]
for p in p_list:
p.start()
for p in p_list:
p.join()
print('主进程')

我们来分析一下这个过程,我们执行了 p1-p4.start() 之后系统中就已经有了四个并发的进程了,在开始运行之后又对 p1.join() ,这时 p1.join() 只会卡住主进程,而 p1 不结束主线程就会一直卡在原地,但其余的 p2-p4 仍然会继续运行。在主进程等待 p1 运行结束时,可能 p2-p4 早已经结束了,这样 p2-p4.join 直接通过检测,无需等待,所以4个 join() 花费的总时间仍然是耗费时间最长的那个进程运行的时间。
三、其他方法或属性
1、terminate() 和 is_alive()
python
#进程对象的其他方法一:terminate,is_alive
from multiprocessing import Process
import time
import random
import os
class MyProcess(Process):
def __init__(self,name):
self.name=name
super().__init__()
def run(self):
print('%s is running' % self.name, os.getpid())
time.sleep(random.randrange(1, 3))
print('%s is done' % self.name, os.getpid())
p1=MyProcess('子进程1')
p1.start()
p1.terminate() # 关闭进程,不会立即关闭,所以is_alive立刻查看的结果可能还是存活
print(p1.is_alive()) # 结果为True
time.sleep(1)
print('开始')
print(p1.is_alive()) # 结果为False

2、pid
- pid:指的是当前进程的 id
- ppid:指当前进程的父进程的 pid
python
from multiprocessing import Process
import time
import random
import os
class MyProcess(Process):
def __init__(self,name):
# self.name=name
# super().__init__()
# Process的__init__方法会执行self.name=MyProcess-1,
# 所以加到这里,会覆盖我们的self.name=name
super().__init__()
self.name = name
def run(self):
print('%s is running,pid is %s,ppid is %s' % (self.name, os.getpid(), os.getppid()))
time.sleep(random.randrange(1, 3))
print('%s is done,pid is %s,ppid is %s' % (self.name, os.getpid(), os.getppid()))
if __name__ == '__main__':
p1=MyProcess('子进程1')
p1.start()
print("%s is p1 process pid." % p1.pid)

僵尸进程与孤儿进程
一、僵尸进程
僵尸进程(Zombie Process)是一种在 UNIX 操作系统中出现的现象。指的是一个已经完成运行任务的子进程,但其父进程没有调用 wait() 或者 waitpid() 获取子进程的状态信息并对该子进程进行善后处理(回收资源),那么这种进程的描述符仍然保存在系统当中继续占用系统资源。
我们对僵尸进程的形成进一步分析。在 UNIX/Linux 系统当中,子进程通常都是通过父进程的建立,然后子进程再创建新的进程。而且子进程和父进程的运行和结束是一个异步的过程,即父进程无法预测子进程什么时候会结束,那就是说无法在子进程结束运行后就立马回收所有资源,而是要留下一部分的描述符给父进程获取子进程的状态信息。这就是 UNⅨ 提供的一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:
- 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,包括进程号(the process ID) 、退出状态(the termination status of the process)、运行时间(the amount of CPU time taken by the process)等
- 直到父进程通过 wait() 或 waitpid() 来获取该信息才释放。但这就造成了另一个问题了,如果父进程一直不调用 wait() 或 waitpid() 的话,那就要一直保留那段信息就不释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的僵尸进程存在,将会因为没有可用的进程号而导致系统不能产生新的进程,可以说危害相当之大
僵尸进程是每个子进程(init除外)结束时都会经历的,我们可以使用 ps 命令查看子进程的状态,如果是僵尸进程状态会是"Z",但有的时候使用 ps 命令并不能查看得到,这是因为 ps 命令来不及看到就已经被父进程处理了。在这过程当中,如果父进程提前于子进程结束,那子进程将会由 init 接管,当子进程进入僵尸进程状态后将会以父进程的身份进行处理。
1、测试
python
#coding:utf-8
from multiprocessing import Process
import time,os
def run():
print('子进程:',os.getpid())
if __name__ == '__main__':
p=Process(target=run)
p.start()
print('主进程:',os.getpid())
time.sleep(1000)
2、解决方法
正常的流程是等待父进程正常结束后调用 wait() 或 waitpid() 去回收僵尸进程,但如果父进程是一个死循环,那么该僵尸进程就会一直存在,对系统造成危害。
解决方法一:使用 kill -9 pid 命令杀死父进程
解决方法二:对开启的子进程使用 join()
python
# 引用 Python3 中的源码注释
class BaseProcess(object):
...
...
def join(self, timeout=None):
'''
Wait until child process terminates
'''
self._check_closed()
assert self._parent_pid == os.getpid(), 'can only join a child process'
assert self._popen is not None, 'can only join a started process'
res = self._popen.wait(timeout)
if res is not None:
_children.discard(self)
...
...
# join() 中调用了 wait(),告诉系统释放僵尸进程
# 而 discard() 是把僵尸进程从自己的 _children 中剔除掉
解决方法三:使用 signal 模块
python
import signal
...
...
# 在父进程中加入该语句来忽略 SIGCHLD 信号
signal(signal.SIGCHLD, signal.SIG_IGN)
...
...
"""
SIGCHLD 信号:该信号是子进程改变状态的时候(比如子进程终止或停止)向父进程发送的,当我们使用 SIG_IGN 忽略该信号后,子进程的终止将会由 init 进程来接管。
具体请参考这篇博客:http://blog.csdn.net/u010571844/article/details/50419798
"""
二、孤儿进程
孤儿进程(Orphan Process)是指一个子进程在其父进程结束或者意外终止后,仍然在系统中运行的进程。此时内核就把孤儿进程托管给 init 进程,并由 init 进程对它们完成状态收集工作之类的善后工作,而 init 进程会循环地 wait() 它的已经退出的子进程,这样孤儿进程自然而然就会结束其的生命周期。因此孤儿进程并不会有什么危害。
1、测试
python
# 需要在 UNIX/Linux 中运行测试
import os
import sys
import time
pid = os.getpid()
ppid = os.getppid()
print 'im father', 'pid', pid, 'ppid', ppid
pid = os.fork()
# 执行pid=os.fork()则会生成一个子进程
# 返回值pid有两种值:
# 如果返回的pid值为0,表示在子进程当中
# 如果返回的pid值>0,表示在父进程当中
if pid > 0:
print 'father died..'
sys.exit(0)
# 保证主线程退出完毕
time.sleep(1)
print 'im child', os.getpid(), os.getppid()
2、解决方法
系统的 init 进程会接管,无需处理。
对于僵尸进程和孤儿进程想了解更多的可以看看这篇博客: https://www.cnblogs.com/Anker/p/3271773.html