并发编程(三)

前言

经过前一篇博客的学习了解了 Unix 和 Windows 系统创建进程的方式了,对于 Unix系统来说,会把父进程的数据直接拷贝一份到子进程的内存空间;而 Windows 系统会重新加载一遍父进程的代码.

那么在 python 中怎么创建进程呢?其实也是调用了操作系统提供的接口,像 Unix 是 fork 接口, Windows 是 CreateProcess 接口.

python 进程

Process 源码探析

首先不管怎么创建进程都是调用了一个multiprocessing模块里面的 Process 类,学习一个模块第一件事就是查看源码.(ps: 对我来说看源码很爽😋)因为 python3看不到源码,所以用 python2可以看到:

翻译 Process 类的注释:进程对象表示在隔开的进程中运行的活动,这个类和 threading.Thread 同义.(因为最开始的计算机都是单核,多进程是后面才出来的)

那么很显然,因为 Process 是个类,那么想要创建一个进程就是实例化一个 Process 类的对象.查看__ init__方法:

image-20181116211024312

哇,参数好多啊,不过大部分都可以使用默认值,第一个参数是 group, 看后面有一句注释: group 参数当前必须为 None, 好了可以不用理会了;第二个参数很重要,表示创建的进程将要进行的任务,必须要传参数(函数名);第三个是名字,可以自定义进程名;第四个是可变长参数,参数会在创建进程的时候传进 target 中;第五个为关键词参数,也是给 target 传的.

assert group is None

断言,只有该表达式值为 True 才会运行下面的代码.很显然不用管就行.

然后下面的都是一些类属性,需要关注的是 self._target,sekf._popen.

查看 process 模块的注释:

翻译可知:

模块提供 Process 类是模拟于’threading.Thread’

再分配和使用的源代码和二进制形式,无论有无修改,但必须符合下列的情况:

1.源代码的再分发必须保留上述版权声明,此条件列表和以下免责声明。

2.二进制形式的再分发必须在随分发提供的文档和/或其他材料中复制上述版权声明,此条件列表和以下免责声明。

3.未经事先书面许可,不得使用作者姓名或任何贡献者的姓名来认可或宣传本软件衍生的产品。(ps: 这注释怎么和 process 一点关系没有😲)

开启进程的两种方式

  1. 实例化 Process 类

创建一个子进程的 demo 如下:

from multiprocessing import Process,current_process

def task():
    print('子', current_process)
    
if __name__ == '__main__':
    p = Process(target=task)
    p.start() # 启动一个进程
    print('主', current_process)

注意:创建进程的代码为什么要放在 mian 下面?

这是因为在 windows 系统下创建进程会重新加载一遍父进程的代码,如果不放在 main 判断下面的话会重复执行创建进程的代码.在 类linux 系统下就不用了.创建进程后执行start方法其实就是运行传入的 task 函数:

这个类方法的注释含义为:运行在子进程中的方法,可以在子类中重写.

看看创建的子进程的运行结果:

结果会打印出主进程和子进程.

  1. 新建一个继承自 Process 类的子类并改写 run 方法

demo 如下:

from multiprocessing import Process,current_process

class MyProcess(Process):
    def run(self):
        print('子', current_process())


if __name__ == '__main__':
    p = MyProcess()
    p.start()
    print('主', current_process())

运行结果:

进程之间内存空间互相隔离

from multiprocessing import Process

x = 100
def task():
    global x
    x = 1
    print('子', x)
    
if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    print('主', x)

运行结果:

可以得出即使在子进程中global x了,修改的也是子进程内存空间里面的名称,这和之前讲的子进程将父进程的代码重新加载了一遍,所以这里面的 x 是两个不同的 x.

[image-20181117110113666](https://ws1.sinaimg.cn/large/006tNbRwly1fxb1zr8r3ij315e04o3z8.jpg

为什么进程的内存空间需要切必须要互相隔离呢?

进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术.这个技术是为了避免A 进程写入 B 进程的情况发生.进程的隔离实现,使用了虚拟地址空间.进程 A 的虚拟地址和进程 B 的虚拟地址不同,这样就防止进程 A 将数据信息写入进程 B,总的来说就是为了数据安全,但也有办法可以实现进程间通信,稍后再谈.

父子进程执行顺序与 join 方法

在上面的代码中实例一个进程对象然后执行 start 方法,会创建出一个子进程然后去执行任务,其实 python 只是调用了操作系统提供的接口,在上一篇博客说到,类 Unix 是调用了操作系统的 fork 函数, windows 是 CreateProcess 函数,所以是通过操作系统来调用并创建一个进程的,而创建进程需要一些必要的资源,那么在操作系统分配这些资源的过程中,主进程代码的执行应该进行完成了,所以运行结果会先出现主进程代码执行完,后子进程的代码执行完.

创建进程的具体时间

from multiprocessing import Process
import time


def task():
    start_time = time.time()
    print('子进程运行时间')
    print(time.time() - start_time)


if __name__ == '__main__':
    start_time = time.time()

    p = Process(target=task)
    p.start()
    print('创建子进程', time.time() - start_time)

    print('主', time.time() - start_time)

运行结果:

可以得出创建子进程几乎花了总程序运行时间的90%以上,所以主进程先运行结束就很正常了.

join 方法

那么如何可以让主进程等待子进程运行结束主进程才结束呢?

from multiprocessing import Process
import time


def task():
    start_time = time.time()
    print('子进程运行时间')
    print(time.time() - start_time)


if __name__ == '__main__':
    start_time = time.time()

    p = Process(target=task)
    p.start()
    print('创建子进程', time.time() - start_time)
    p.join()

    print('主', time.time() - start_time)

运行结果:

这样主进程就会等待子进程运行结束才会结束.

join 方法:主进程等待子进程运行完毕,即主进程在原地阻塞而不影响子进程的运行.

进程对象相关属性和方法

from multiprocessing import Process
import os,time

def task(name):
    print('start', name)
    time.sleep(5)
    print('stop', name)
    
if __name__ == '__main__':
    p = Process(target=task, args=('musibii',), name='musibii_Process')
    p.start()
    
    print(p.name) # 获取进程名,可以自定义
    print(p.pid) # 获取进程 pid
    p.terminate() # 结束子进程
    print(p.is_alive()) # 判断进程是否存活,布尔值
    
    print(os.getpid()) # 当前主进程 pid
    print(os.getppid()) # 执行 py 文件的进程,当前为 pycharm 进程pid

运行结果:

为什么在 terminate 之后判断子进程是否存活,结果为 True 呢?

因为在执行终结子进程命令后需要操作系统来结束子进程,而完全终结子进程需要一定的时间,而代码执行速度很快,所以会是 True.

僵尸进程与孤儿进程以及守护进程

僵尸进程

在类 Unix 系统中,僵尸进程是指完成执行(通过 exit 系统调用,或运行时发生致命错误或收到终止信号所致)但在操作系统的进程表中仍然有一个表项(进程控制块 PCB),处于’终止状态’的进程.

孤儿进程

在操作系统中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程,这类进程由操作系统进行管理和回收.

守护进程

在一个多工的电脑作业系统中,守护进程是一种在后台执行的电脑程序.此类程序会被以进程的形式初始化.守护进程程序的名称通常以’ d’结尾:例如 syslogd 就是指管理系统日志的守护进程.

详解:

  1. 一般情况下,子进程是由父进程创建的,而子进程和父进程的退出是无顺序的,两者之间都不知道谁先退出.正常情况下父进程先结束则会调用 wait 或者 waitpid 函数等待子进程完成再退出,而一旦父进程不等待直接退出,则剩下的子进程会被 init(pid=1)进程接收,成被孤儿进程.(进程树种除了 init 都会有父进程)
  2. 如果子进程先退出,父进程还未结束并且没有调用 wait 或者 waitpid 函数获取子进程的状态信息,则子进程残留的状态信息(task_struct 结构和少量系统资源信息)会变成僵尸进程.
  3. 守护进程是指在后台运行,没有控制终端与之相连的进程.它独立于控制终端,通常周期性的执行某种任务.(特别的,守护进程不能有子进程)守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断.

产生的危害

孤儿进程结束后会被 init 进程管理并处理后事,并没有危害,而僵尸进程则会一直占着进程号,操作系统的进程数量有限则会受影响.

解决办法:

一般僵尸进程的产生都是因为父进程的原因,则可以通过kill 父进程解决,这时候僵尸进程就变成了孤儿进程,被 init 进程管理.