进程的概念:(Process)
- 进程就是正在运行的程序,它是操作系统中资源分配的最小单位。
- 资源分配:操作系统分配的CPU时间片、内存、磁盘空间端口等等资源。
- 进程号(process identification)是操作系统分配给进程的唯一标识号,用户每打开一个进程操作系统都会为其创建PID。
- UID:用户id; PID:进程id; PPID:父进程id。
- 在存储空间中未被执行的叫程序,被执行的叫进程(进行中的程序)。
- 同一个程序执行两次之后是两个进程。
- 进程和进程之间的关系:进程之间数据隔离,但可通过socket通信。
并行和并发
- 并发:单个CPU通过操作系统调度以极快地速度轮流执行多个进程,给用户的感觉是多个进程正在同时执行。并发在逻辑上是同时执行,实质上是轮流执行。
- 并行:多个CPU在同一时间分别执行不同的进程。并行是真正的同时执行。
CPU调度策略
-
先来先服务:先来的先执行。
-
短作业优先:短作业优先执行。
-
时间片轮转:每个作业执行一个时间片后轮转其它作业执行。
-
多级反馈队列:
- 进程首次启动时进入优先级最高的Q1队列等待;
- 优先执行Q1队列中的进程;若高优先级Q1队列中无待执行的进程,那么会执行Q2队列中的进程;若Q1、Q2队列中都无待执行的进程,那么会执行Q3队列中的进程;以此类推,直至末尾队列的进程。
- 对同级队列中的进程按先来先服务的策略分配时间片。如Q1队列的时间片为N,若Q1中的进程用完时间片N后还未完成时会被调整到Q2队列;若用完Q2队列的时间片后还未完成时会被调整到Q3队列;以此类推,直至被调整到末尾队列。
- 在末尾队列QN中的各个进程,按照时间片轮转执行。
- 如果低优先级队列中的进程在运行时有新进程需要运行,那么它会被中断运行并被放入当前队列的队尾,然后让新进程优先运行。另外被中断运行的进程再度运行时它只能得到上次未用完的时间片。
- 优先级越高的队列时间片越短,优先级越低的队列时间片越长。如三级反馈队列Q1、Q2、Q3它们的时间片分别为2、4、8。
进程三状态
- 就绪(Ready)状态:等待被CPU执行的状态。
- 执行(Running)状态:正在被CPU正在执行的状态。
- 阻塞(Blocked)状态:等待某个事件发生(如等待用户输入)而无法执行的状态。
代码示意图:
同步与异步、阻塞与非阻塞
同步与异步、阻塞与非阻塞发生在多任务场景中:
- 同步:是指A进程调用B进程后,A进程要等B进程完成后才能继续运行,这是单进程运行状态。上面的代码示意图中就是同步。
- 异步:是指A进程调用B进程后,A进程不等B进程完成,它和B进程可以同时执行,这是多进程(或线程)运行状态。
- 阻塞:是指进程调用了某些I/O操作后进入挂起状态,要等I/O返回的结果才继续执行。
- 非阻塞:是指进程调用了某些I/O操作后不进入挂起状态,不用等待I/O返回的结果就继续执行。
四种状态:
同步阻塞:单进程运行有阻塞事件时,等待阻塞事件完成后才能继续运行。
异步阻塞:某进程运行时有多条子进程(或线程)同步运行,它的某条子进程有阻塞事件进入阻塞状态,而其它子进程仍然正常运行。
同步非阻塞:没有阻塞事件,单进程正常执行的状态。
异步非阻塞:多进程运行且无阻塞事件。
多进程
-
处理多进程的模块:
注意:导入的是Process类,首字母必须大写,另外小写的是process文件。
from multiprocessing import Process
-
函数式编程方式:
案例:
from multiprocessing import Process import os def func(n): for i in range(n): print("func", os.getpid(), os.getppid()) if __name__ == '__main__': print("main", os.getpid(), os.getppid()) p = Process(target=func, args=(1,)) p.start() out: main 6928 6621 func 6929 6928
代码说明:
- os.getpid:获取进程号。
- os.getppid:获取父进程号。
- p = Process(target=func, args=(1,)):创建Process类的实例,目标是func函数,args=(1,)是给函数传参数(注意agrs=后面必须是元祖,1后面的逗号不能少,少了就不是元祖,会报错),以此创建子进程。
-
面向对象式编程方式:
案例:
from multiprocessing import Process from time import sleep from os import getpid, getppid class MyProcess(Process): def __init__(self, n): self.n = n super().__init__() def run(self): sleep(0.1) print(f"第{self.n}次打印,子进程id:{getpid()},父进程id:{getppid()}") if __name__ == "__main__": for i in range(5): MyProcess(i).start() print("*" * 20) out: ******************** 第0次打印,子进程id:13029,父进程id:13028 第2次打印,子进程id:13031,父进程id:13028 第1次打印,子进程id:13030,父进程id:13028 第3次打印,子进程id:13032,父进程id:13028 第4次打印,子进程id:13033,父进程id:13028
代码说明:
- 面向对象编程必须自定义一个类并继承Process类。
- 必须使用__init__传参,在传参完成后必须调用父类的__init__方法才能正确完成初始化。
- 创建子进程的方法就是初始化自定义类的方法。
-
Process类常用的属性和方法:
先看Process类源码:
class Process(process.BaseProcess): _start_method = None @staticmethod def _Popen(process_obj): return _default_context.get_context().Process._Popen(process_obj)
Process类本身内容很少,但它继承了process.BaseProcess类,再来看process.BaseProcess类:
class BaseProcess: name: str daemon: bool authkey: bytes def __init__( self, group: None = ..., target: Optional[Callable[..., Any]] = ..., name: Optional[str] = ..., args: Tuple[Any, ...] = ..., kwargs: Mapping[str, Any] = ..., *, daemon: Optional[bool] = ..., ) -> None: ... def run(self) -> None: ... def start(self) -> None: ... def terminate(self) -> None: ... if sys.version_info >= (3, 7): def kill(self) -> None: ... def close(self) -> None: ... def join(self, timeout: Optional[float] = ...) -> None: ... def is_alive(self) -> bool: ... @property def exitcode(self) -> Optional[int]: ... @property def ident(self) -> Optional[int]: ... @property def pid(self) -> Optional[int]: ... @property def sentinel(self) -> int: ...
代码说明:
- run方法:即多进程需要执行的代码主体。
- start方法:即多进程启动运行。
- self.terminate方法:强制终止子进程。请注意这个方法是调用操作系统来关闭子进程,通常需要零点零零几秒的片刻时间后该子进程才会被终止。这是异步非阻塞的方法,即该方法通知操作系统后会立即继续执行后续代码,它不等待操作系统返回结果,后续代码运行的时候操作系统杀进程的代码也在同步运行的。
- is_alive方法:查看子进程是否活着。用self.terminate方法结束子进程后立即查看可能还是True即活着的状态,要等待操作系统执行完杀进程的操作后才会返回False即死了的状态。
- self.pid和self.ident属性:它们是被property装饰的方法,返回当前进程的id,这2个属性内容完全一致。
- self.exitcode属性:返回子进程结束时的状态码。
-
不同操作系统平台下的差异:
- windows平台下创建子进程是通过加载py文件来获取所需的数据和代码,假如不写“ if __name__ == ‘__main__’ ”会造成递归加载py文件导致加载失败!
- linux和mac平台下创建子进程是通过拷贝父进程内存空间来获取的所需的数据和代码,所以在linux和mac平台下不写“ if __name__ == ‘__main__’ ”也可以正常执行,不会导致加载失败!
- 在linux平台下创建和运行子进程的效率比window平台下高得多。
-
不同子进程之间内存隔离,不能直接共享数据。但是可以通过socket通信。
-
开启多个子进程的示范:
from multiprocessing import Process import time def func(name): time.sleep(0.5) print('子进程:', name) if __name__ == '__main__': print("父进程:") name_list = ['张三', '李四', '王五'] for i in name_list: p = Process(target=func, args=(i,)) p.start() out: 父进程: 子进程: 张三 子进程: 王五 子进程: 李四
代码说明:
- 在该案例中time.sleep(0.5)是阻塞事件,多条子进程各自执行,遇到阻塞时各自等待,相互不干扰。这就是异步阻塞。
- 可以使用循环的方式创建多条子进程。
-
join阻塞主进程,主进程等待被join的子进程运行结束后才继续运行:
-
模拟多进程下载文件的错误代码:
from multiprocessing import Process import time import random def func(name): time.sleep(random.random()) print('子进程:', name) if __name__ == '__main__': name_list = ['下载完第一部分', '下载完第二部分', '下载完第三部分', '下载完第四部分', '下载完第五部分'] for i in name_list: p = Process(target=func, args=(i,)) p.start() print("文件五个部分下载完成,合并完毕!") out: 文件五个部分下载完成,合并完毕! 子进程: 下载完第四部分 子进程: 下载完第一部分 子进程: 下载完第五部分 子进程: 下载完第二部分 子进程: 下载完第三部分
代码说明:
- 上述代码模拟多进程下载文件。假设不阻塞主进程,那么这个程序无法保证正确执行。
- 要保证上述代码正常运行就必须阻塞主进程,等待所有子进程下载完毕后主进程才能继续执行后续的合并和校验文件以及告知用户下载完成的工作。
-
模拟多进程下载文件的正确代码:
from multiprocessing import Process import time import random def func(name): time.sleep(random.random()) print('子进程:', name) if __name__ == '__main__': name_list = ['下载完第一部分', '下载完第二部分', '下载完第三部分', '下载完第四部分', '下载完第五部分'] process_list = [] for i in name_list: p = Process(target=func, args=(i,)) p.start() process_list.append(p) for i in process_list: i.join() print("文件五个部分下载完成,合并完毕!") out: 子进程: 下载完第二部分 子进程: 下载完第五部分 子进程: 下载完第一部分 子进程: 下载完第三部分 子进程: 下载完第四部分 文件五个部分下载完成,合并完毕!
代码说明:
- 在上述代码中,每次创建并开启子进程后,会将子进程的对象内存地址存入process_list列表中。
- 所有子进程创建完毕后,遍历process_list列表,将所有子进程对象设为阻塞事件,设置方法是p.join().
-
-
守护进程:
-
给子进程设置守护进程属性为True,该子进程会随着主进程代码执行完毕而结束。
p.daemon = True
-
设置守护进程属性语句必须在子进程启动语句前面。
p.daemon = True p.start()
-
守护进程内无法再开启子进程。
-
案例,看案例请思考一个问题,son1的打印语句会执行几次?
import time from multiprocessing import Process def son1(): while True: print('->1号子进程') time.sleep(1) def son2(): for i in range(5): print('->2号子进程') time.sleep(1) if __name__=="__main__": p1 = Process(target=son1) p1.daemon = True p1.start() p2 = Process(target=son2) p2.start() print("->主进程") time.sleep(3) out: ->主进程 ->1号子进程 ->2号子进程 ->1号子进程 ->2号子进程 ->1号子进程 ->2号子进程 ->2号子进程 ->2号子进程
代码说明:
- 在上述案例中,son1函数即1号子进程每隔1秒打印"->1号子进程"(无限循环);
- son2函数即2号子进程每隔1秒打印"->2号子进程"(循环5次);
- 在主进程代码中对son1设守护进程属性为True,然后启动son1子进程;
- 对son2未设守护进程属性(默认为False),然后启动son2子进程;
- 主进程sleep3秒,显示结果是son1打印了3次。即主进程的代码3秒执行完毕后son1子进程会被强制结束!
- son2子进程未设daemon属性,它正常打印了5次,它不会随着主进程的代码结束而结束。
- 结论:子进程daemon属性为True的是守护进程,在主进程代码结束时它会被强制结束;子进程daemon属性为False的是非守护进程,在主进程代码结束时它仍然会正常运行直至运行完毕;主进程代码结束后守护进程会立即结束,之后python解释器还会做一些回收资源的工作,最后主进程才真正结束。
-