并发编程——进程——Process对象的属性和方法
pid属性和ppid属性
其实到现在为止,都是我说咱们是另外开启了一个进程,并没有实际证据证明,怎么证明呢?
我们知道,程序中的变量都是由id的,其实就是内存地址,那么,进程也可以查看自己的id,进程的id叫pid,也就是progress id,python的os模块提供了相应的方法,还有ppid,也就是parent progress id,可以查看父进程的id。
另外,我们通过源代码的学习知道Process其实就是一个类而已,那么我们可以通过继承创建自己的MyProcess类,肯定也能实现同样的效果,来看一下代码:
import os
import time
from multiprocessing import Process
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = name
def run(self):
print(self.name, " id = ", os.getpid())
print(self.name, " father process id = ", os.getppid())
if __name__ == '__main__':
MyProcess("Process 1").start()
print("Main process id = ", os.getpid())
print("Main process's father process id = ", os.getppid())
time.sleep(10)
输出结果为:
Main process id = 10428
Main process's father process id = 10560
Process 1 id = 16636
Process 1 father process id = 10428
首先是定义MyProcess继承自Process类,在MyProgress类中必须定义一个名叫run的方法,就类似于之前定义的task函数,函数中编写子进程的业务逻辑。
为什么一定要定义run方法,这个问题还是要扒源码,现在的继承关系是:
MyProcess<——Process<——process.BaseProcess
来到BaseProcess类中,可以发现类中有一个run方法:
def run(self):
'''
Method to be run in sub-process; can be overridden in sub-class
要在子进程中运行的方法;可以在子类中重写。
'''
if self._target:
self._target(*self._args, **self._kwargs)
其实原本的run方法就是运行target函数,只不过我们如今在MyProcess类中重写了,直接在run方法中编写业务逻辑。
然后我们来分析一下程序输出结果:主程序的pid是10428,子进程的pid是16636,这显然不是同一个进程,说明我们真的新起了一个进程,子进程的ppid是10428,正好是主进程的pid,说明子进程确实是主进程的子进程,这样就证明了“我儿子是我儿子”这么一个奇葩的命题。
我们说,每个进程都有自己父进程,那么,主进程的父进程是谁,可以通过输出结果看到主进程的父进程pid是10560,其实,在主程序最后睡眠的10秒钟内,我们打开任务管理器找到详细信息中的python查看PID:
现在我们知道了,所运行代码的父进程就是python解释器。
is_live方法、terminate方法、join方法
来思考一个问题,如果我在子进程中修改了全局变量,等到子进程执行完毕之后,在主进程中访问这个全局变量,输出的结果会不会被更改呢?
这样,实践是检验真理的唯一标准,我们来跑代码试试,在coding之前,我们要解决一个问题,我怎么知道子进程什么时候执行完毕,最简单的办法,开启子进程之后让主进程先睡它个十秒钟,足够子进程执行了。
sleep当然可以,但这也太low了,有可能我子进程1秒钟就执行完了,那我这不白等了9秒钟么,浪费时间就是浪费生命啊。
那这样,我先判断子进程是不是还活着,如果子进程还活着就说明子进程还没有执行完毕,等他死呗,正好Process对象提供了is_alive方法可以判断子进程的死活。
事实上还有更好的解决方案,Process类也给我们提供了现成的join方法,它让主线程在join的点等待子线程执行完毕,再继续执行剩下的主进程代码,这不就省去我们自己来判断了么。
但就算这样,还有一个问题,万一我子进程一直不结束怎么办?子进程里要是有个死循环那我不是要等到天荒地老,为了解决这个问题啊,再引入一个terminate方法,它可以直接干掉子进程。
import time
import multiprocessing
def task(process_name, sleep_time):
global number
number = sleep_time
print(process_name, "number =", number)
time.sleep(sleep_time)
print(process_name, "pid = ", multiprocessing.current_process().pid)
number = 10
if __name__ == '__main__':
process1 = multiprocessing.Process(target=task, args=("process1", 2))
process2 = multiprocessing.Process(target=task, args=("process2", 1))
process1.start()
start_time = time.time()
while process1.is_alive():
duration = time.time() - start_time
if duration > 2:
# 如果子进程超过两秒还没执行完,直接干掉
process1.terminate()
print("Process1 is terminate, survival: ", process1.is_alive())
time.sleep(0.1)
break
print("After terminal process1 is alive: ", process1.is_alive())
print("After process 1 done, number =", number)
print()
process2.start()
print("After process2 start.")
process2.join()
print("Process2 is alive:", process1.is_alive())
print("Process2 pid =", process2.pid)
print("After process2 done, number =", number)
输出结果为:
process1 number = 2
Process1 is terminate, survival: True
After terminal process1 is alive: False
After process 1 done, number = 10
After process2 start.
process2 number = 1
process2 pid = 20740
Process2 is alive: False
Process2 pid = 20740
After process2 done, number = 10
先看一下task函数,两个参数,一个是进程的名字,一个是沉睡的时间,首先把全局变量number改为沉睡时长,然后打印number,之后沉睡,最后打印当前进程pid。
再看一下主函数,首先声明两个进程process1和process2,开启process1,记录开启时间,然后在while循环中判断process1是否还活着,如果process1超过两秒还没执行完,直接干掉。
看一下输出结果的前两行,说明在process1中number被改为了2,但是terminate之后判断process1是否还活着输出结果竟然是True,没事 ,先别慌,terminate之后先睡0.1秒再break,再看process是否还活着,这时候输出False了。
因此可以知道,process1执行terminate之后并不会马上被干掉。
继续往下看,确认process1死了之后,我们发现task函数中最后一行打印进程pid并没有执行,这说明process1是真死的透透的了。
打印number的值,发现number的值为10,并不是前边输出的process1中number=2,所以,如果我在子进程中修改了全局变量,等到子进程执行完毕之后,主进程中全局变量并没有被更改,这说明子进程跟主进程的内存空间是隔离的。
最后我们通过join方法处理process2可以很简便的实现上述步骤,但是啊,我们好好看一下,我们在process2中打印当前进程pid为20740,然后等process2结束,并且判定process2已经死亡后,再输出process2的pid,发现仍可以输出20740,这么一看的话,子进程死了之后主进程还可以获取子进程的一些信息。
子进程都TM死了还能获取信息,这就变成了僵尸进程啊。
僵尸进程
那到底什么是僵尸进程呢?
如果子进程退出,而父进程没有调用wait或waitpid获取子进程的状态信息,那么子进程的描述信息扔保存在系统中,这种进程称之为僵尸进程。
父进程和子进程的运行是一个异步的过程,所以父进程永远无法预测子进程到底什么时候结束,如果子进程结束后立刻回收其全部资源,那么在父进程内将无法获取子进程的描述信息。
因此,必须有一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息。
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。
但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等),直到父进程通过wait / waitpid来取时才释放。
但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。
这是每个子进程在结束时都要经过的阶段。
孤儿进程
跟僵尸进程类似的是孤儿进程,当一个父进程退出,而它的子进程还在运行,那么这些子进程将成为孤儿进程。
孤儿进程将被init进程(PID=1)所收养,并由init进程对它们完成状态收集工作。
简单的说,孤儿进程是没有父进程的进程,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()孤儿进程的已经退出的子进程。
这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。
因此孤儿进程并不会有什么危害。
练习题1:改写下列程序,实现按数字顺序打印效果
import time
import random
from multiprocessing import Process
def task(n):
time.sleep(random.randint(1, 3))
print('-------->%s' % n)
if __name__ == '__main__':
p1 = Process(target=task, args=(1,))
p2 = Process(target=task, args=(2,))
p3 = Process(target=task, args=(3,))
p1.start()
p2.start()
p3.start()
print('-------->4')
守护进程
之前我们说子进程是不会随着主进程的结束而结束,子进程全部执行完之后,程序才结束,那么如果有那么一个需求:主进程结束后由主进程创建的子进程必须跟着结束。
这就像以前的皇帝一样,皇帝死了一大堆人得陪葬。
这时候,就需要用到守护进程了,主进程创建守护进程,守护进程在主进程代码执行结束后就终止,并且守护进程内无法再开启子进程。
import time
import random
import multiprocessing
class MyProcess(multiprocessing.Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = name
def run(self) -> None:
print(self.name, "started on:", time.asctime(time.localtime(time.time())))
time.sleep(random.randint(1, 4))
print(self.name, "end on:", time.asctime(time.localtime(time.time())))
if __name__ == '__main__':
process1 = MyProcess("Process 1")
process1.daemon = True
process1.start()
time.sleep(random.randint(1, 4))
print("Main process is done.")
这是一段神奇的代码,多次执行得到的结果可能不一样:
Main process is done.
Process 1 started on: Thu Feb 6 19:41:49 2020
Process 1 end on: Thu Feb 6 19:41:51 2020
Main process is done.
Process 1 started on: Thu Feb 6 19:42:02 2020
Main process is done.
练习题2:看代码写结果
import time
from multiprocessing import Process
def foo():
print("Foo start.")
time.sleep(1)
print("Foo done.")
def bar():
print("Bar start.")
time.sleep(3)
print("Bar done.")
if __name__ == '__main__':
p1 = Process(target=foo)
p2 = Process(target=bar)
p1.daemon = True
p1.start()
p2.start()
print("Main done.")
练习题
1:改写下列程序,实现按数字顺序打印效果
import time
import random
from multiprocessing import Process
def task(n):
time.sleep(random.randint(1, 3))
print('-------->%s' % n)
if __name__ == '__main__':
p1 = Process(target=task, args=(1,))
p2 = Process(target=task, args=(2,))
p3 = Process(target=task, args=(3,))
p1.start()
p2.start()
p3.start()
print('-------->4')
import time
import random
from multiprocessing import Process
def task(n):
time.sleep(random.randint(1, 3))
print('-------->%s' % n)
if __name__ == '__main__':
p1 = Process(target=task, args=(1,))
p2 = Process(target=task, args=(2,))
p3 = Process(target=task, args=(3,))
p1.start()
p1.join()
p2.start()
p2.join()
p3.start()
p3.join()
print('-------->4')
2:看代码写结果
import time
from multiprocessing import Process
def foo():
print("Foo start.")
time.sleep(1)
print("Foo done.")
def bar():
print("Bar start.")
time.sleep(3)
print("Bar done.")
if __name__ == '__main__':
p1 = Process(target=foo)
p2 = Process(target=bar)
p1.daemon = True
p1.start()
p2.start()
print("Main done.")
Main done.
Bar start.
Bar done.