Python学习第35天之进程part1

一、进程的理论基础

1、进程就是一个程序在一个数据集上的一次动态执行过程。是用来描述程序执行过程的虚拟概念。进程的概念起源于操作系统,进程是操作系统最核心的概念,操作系统其它所有的概念都是围绕进程来的。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志

2、进程与程序的区别

  程序仅仅只是一堆代码而已,而进程指的是程序的运行过程

  需要强调的是:同一程序执行两次,那也是进程,比如登录QQ,虽然都是同一个软件,但是一个可以视频聊天,一个可以逛空间。

3、并发和并行

并发:单CPU,多进程并发

  无论是并行还是并发,在用户看来都是 “同时” 运行的,不管是进程还是线程,都只是一个任务而已,真实干活的是 CPU,CPU 来做这些任务,而一个 CPU 同一时刻只能执行一个任务

  并发是伪并行,即看起来是同时运行。单个 CPU + 多道技术就可以实现并发(并行也属于并发)

并行:多CPU(同时运行,只有具有多个cpu才能实现并行)

  单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的

  有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了 CPU1,CPU2,CPU3,CPU4,一旦任务 1 遇到 I/O 就被迫中断执行,此时任务 5 就拿到 CPU1 的时间片去执行,这就是单核下的多道技术,而一旦任务 1 的 I/O 结束了,操作系统会重新调用它(需知进程的调度、分配给哪个 CPU 运行,由操作系统说了算),可能被分配给四个 CPU 中的任意一个去执行

  所有现代计算机经常会在同一时间做很多件事,一个用户的PC(无论是单 CPU 还是多CPU),都可以同时运行多个任务(一个任务可以理解为一个进程)。

PS:多道技术

多道技术:内存中同时存入多道(多个)程序,CPU 从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个 CPU 只能执行一个任务,但在 1 秒内,CPU 却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个 CPU 共享同一个物理内存)

4、同步和异步

同步执行:一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行

异步执行:一个进程在执行某个任务时,另外一个进程无需等待其执行完毕,就可以继续执行,当有消息返回时,系统会通知后者进行处理,这样可以提高执行效率

举个例子,打电话时就是同步通信,发短息时就是异步通信。

5、进程的创建

但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为四种形式创建新的进程

   1)系统初始化(查看进程 Linux 中用 ps 命令,Windows 中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、Web 页面、新闻、打印)

  2)一个进程在运行过程中开启了子进程(如 nginx 开启多进程,os.fork,subprocess.Popen 等)

  3)用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4)一个批处理作业的初始化(只在大型机的批处理系统中应用)

无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:

  1)在 UNIX 中该系统调用是:fork,fork 会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在 shell 解释器进程中,执行一个命令就会创建一个子进程)

  2. 在 Windows 中该系统调用是:CreateProcess,CreateProcess 既处理进程的创建,也负责把正确的程序装入新进程。

  关于创建的子进程,UNIX 和 Windows

  1)相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。

  2)不同的是:在 UNIX 中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于 Windows 系统来说,从一开始父进程与子进程的地址空间就是不同的。

6、进程的终止

  1、正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在 Linux 中用 exit,在 Windows 中用 ExitProcess)

  2、出错退出(自愿,python a.py 中 a.py 不存在)

  3、严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0 等,可以捕捉异常,try...except...)

  4、被其他进程杀死(非自愿,如 kill -9)

7、进程的层次结构

无论 UNIX 还是 Windows,进程只有一个父进程,不同的是:

  1)在 UNIX 中所有的进程,都是以 init 进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。

  2)在 Windows 中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。

8、进程的状态

 

9、进程并发的现象

  进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统为每个进程定义了一个数据结构——进程控制块 PCB(Process Control Block)。它是进程实体的一部分,是操作系统中最重要的记录型数据结构。PCB 中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。进程控制块的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其它进程并发执行的进程。或者说,OS 是根据 PCB 来对并发执行的进程进行控制和管理的。PCB 是进程存在的唯一标志。

 

二、进程的使用

1、multiprocessing 模块介绍

Python 中的多线程无法利用多核优势,如果想要充分地使用多核 CPU 的资源(os.cpu_count()查看),在 Python 中大部分情况需要使用多进程。Python提供了 multiprocessing。

multiprocessing 模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块 threading 的编程接口类似。

multiprocessing 模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了 Process、Queue、Pipe、Lock 等组件。

需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

2、Process 类的介绍

(1)创建进程的类

Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,可用来开启一个子进程
 
强调:
1. 需要使用关键字的方式来指定参数
2. args 指定的为传给 target 函数的位置参数,是一个元组形式,必须有逗号

(2)、参数介绍

group参数未使用,值始终为None

target表示调用对象,即子进程要执行的任务

args表示调用对象的位置参数元组,args=(1,2,'qiu',)

kwargs表示调用对象的字典,kwargs={'name':'qiu','age':18}

name为子进程的名称

(3)、方法介绍

p.start():启动进程,并调用该子进程中的 p.run() 
p.run():进程启动时运行的方法,正是它去调用 target 指定的函数,我们自定义类的类中一定要实现该方法  

p.terminate(): 强制终止进程 p,不会进行任何清理操作,如果 p 创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。
如果 p 还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive(): 如果 p 仍然运行,返回 True

p.join([timeout]): 主线程等待 p 终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,
需要强调的是,p.join只能join住 start 开启的进程,而不能 join 住 run 开启的进程

(4)属性介绍

p.daemon:默认值为 False,如果设为 True,代表 p 为后台运行的守护进程,当 p 的父进程终止时,p 也随之终止,并且设定为 True 后,p 不能创建自己的新进程
,必须在 p.start() 之前设置 p.name: 进程的名称 p.pid:进程的pid p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可) p.authkey: 进程的身份验证键,默认是由 os.urandom() 随机生成的
32 字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,
这类连接只有在具有相同的身份验证键时才能成功
(了解即可)

3、Process类的使用

注意:在 Windows 中 Process() 必须放到 if __name__ == '__main__': 下

由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。
    如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。
这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句
中的语句将不会在导入时被调用。
方式一:
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__':
    p = Process(target=task, args=("qiu",))
    # p = Process(target=task, kwargs={"name": "qiu"})

    # p.start()只是向操作系统发送了一个开启子进程的信号, 操作系统才能开启子进程,
    # 涉及到申请内存空间, 要将父进程的数据拷贝到子进程, 要将CPU调到子进程里运行子进程的代码
    # 才会有 is running的显示, 这都是一系列的硬件操作
    # 所以print("")这行代码运行速度要快一些
    p.start()
    print("")

方式二:

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__':
    p = MyProcess("qiu")
    p.start()
    print("")

4、join方法

在主进程运行过程中如果想要并发的执行其他任务,我们可以开启子进程,此时主进程的任务和子进程的任务分为两种情况:

  一种情况是:在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源

  还有一种情况是:如果主进程的任务在执行到某一个阶段时,需要等待子进程执行完毕后才能继续执行,就需要一种机制能够让主进程监测子进程是否运行完毕,在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是 join 方法的作用。 

from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == '__main__':
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p1.join()
    p2.join()
    p3.join()
    print("主进程", time.time() - start)

人会有疑问,既然 join 是等待进程结束,那么我像下面 join 下去,进程不就变成串行了的吗?

  当然不是了,必须明确 join 是让谁等:进程只要 start 就会在开始运行了,所以 p1 到 p3.start() 时,系统中已经有三个并发的进程了,而 p1.join() 是在等 p1 结束,p1 只要不结束主线程就会一直卡在原地,这也是问题的关键。join 是让主线程等,而 p1-p3 仍然是并发执行的,p1.join() 的时候,其余 p2,p3 仍然在运行,等 p1.join() 结束,可能 p2,p3 早已经结束了,这样 p2.join(),p3.join() 直接通过检测,无需等待。所以 3 个 join 花费的总时间仍然是耗费时间最长的那个进程运行的时间,所以这里即便交换 join 的顺序,执行的时间仍然是 3 秒多一点,多出来的那零点几秒是开启进程以及进程切换的时间。

交换顺序

from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == '__main__':
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p3.join()
    p1.join()
    p2.join()

    print("主进程", time.time() - start)

join 是让主进程在原地等待,等待子进程运行完毕,不会影响子进程的执行

5、进程间的内存空间互相隔离

from multiprocessing import Process

n = 100

def task():
    global n
    n = 0

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.join()
    print("主进程内的:", n)

6、僵尸进程与孤儿进程

  僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程

  我们知道在 Unix/Linux 中,正常情况下子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束,如果子进程一结束就立刻回收其全部资源,那么在父进程内将无法获取子进程的状态信息。因此,Unix 提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:

  1、在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号、退出状态、运行时间等)

  2、直到父进程通过 wait/waitpid 来取时才释放。但这样就导致了问题,如果进程不调用 wait/waitpid 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。此即为僵尸进程的危害,应当避免。

  任何一个子进程(init除外)在 exit() 之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在 exit() 之后,父进程没有来得及处理,这时用 ps 命令就能看到子进程的状态是 “Z” 。如果父进程能及时 处理,可能用 ps 命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。  如果父进程在子进程结束之前退出,则子进程将由 init 接管。init 将会以父进程的身份对僵尸状态的子进程进行处理。

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

  孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程身上,init 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

原文地址:https://www.cnblogs.com/ye-hui/p/9932173.html