2021/6/4 并发编程-进程
进程理论:
1. 进程和程序
程序:存放在硬盘上的一大堆代码,其状态是"死"的
进程:程序正在运行的过程,其状态是"活"的
2. 进程调度算法
对于多个进程的执行,都需要抢占CPU资源,而对于CPU仅存在一个的情况下,就需要使用进程调度算法来合理分配CPU资源
先来先服务调度算法
对于多个进程,按照先获取到CPU资源,就先执行的方式,再将该进程的任务完全执行后(包括IO操作),才会去执行下一个进程
缺点:无法合理分配CPU资源,对于重要任务/进程,会因为没有抢占到CPU资源,从而延误
短作业优先调度算法:
对于进程的作业,处理及会计算出该进程的长度,从而根据长度来优先执行短的进程任务
缺点:长作业会因为频繁的短作业从而一直得不到CPU资源
时间片轮转调度算法:
将CPU的处理时间划分成一个一个固定大小的时间片,时间片单位可以是几十毫秒之类,对不同的进程分配不同数量的时间片
如果一个进程在被调度选中后,所分配的时间片执行完毕后,还未执行完任务,那么进程重新回到就绪队列,等待调度分配
缺点:无法合理分配任务优先级
1. 进程时间片用完,任务未执行完毕
2. 进程时间片没用完,因为IO操作从而退出到就绪队列
3. 新创建的高优先级进程,无法立刻被执行
多级反馈队列调度算法:
将CPU的处理时间分成多个队列,例如:高、中、低,其中高优先级队列的时间是中优先级队列的一半,以此类推
对于多个进程,按照先入先出调度算法,进入高优先级队列,如果进程任务能够执行完毕,则释放资源,如果高优先级队列完毕进程任务依然无法执行完毕,则分配到中优先级队列
对于高优先级队列中存在进程,那么立马退出中、低优先级队列,继而执行高优先级队列中的进程
缺点:先入先出调度算法无法合理分配每个队列的资源
时间片轮转+多级反馈队列
将每个队列分成一个一个固定大小的时间片,然后按照调度算法分配给需要CPU资源的进程,再按照多级反馈队列机制来执行
3. 进程的三状态图
创建 --提交--> 就绪态Ready --进程调度--> 运行态Running --释放--> 退出
运行态Running --进程任务未执行完毕--> 就绪态Ready
运行态Running --进程任务中途触发事件--> 阻塞态Blocked
阻塞态Blocked --事件执行完毕--> 就绪态Ready
事件:IO操作,请求缓冲区满等
就绪态Ready:进程已经获取到除CPU资源以外的所有必要资源
运行态Running:进程已经获取到CPU资源,开始执行任务
阻塞态Blocked:进程任务中途触发事件,从而释放资源,等待事件执行完毕
4. 同步和异步
同步:任务提交后,原地阻塞,等待任务返回结果
异步:任务提交后,不会原地阻塞(通过回调机制/函数,获取到返回结果),直接执行其他任务
5. 阻塞和非阻塞
阻塞:即处于进程运行三状态中的阻塞态
非阻塞:即处于进程运行三状态中的就绪态或运行态
最高效的组合:异步+非阻塞
我们自己编写脚本,理想状态就是脚本一直处于就绪态和运行态
进程操作:
1. 创建进程的两种方式
使用Process实例化
from multiprocessing import Process
def xxx(self, a):
函数体
if __name__ == ‘__main__‘:
p = Process(target=xxx, args=(a, ))
p.start()
类的继承
class xxx(Process):
def run(self): # 自动调用
函数体
if __name__ == ‘__main__‘:
p = xxx()
p.start
2. Process类
from multiprocessing import Process
p = Process(
target=函数名,
args=(位置参数)
kwargs={关键字参数}
name=进程别名
group=None # 未使用
)
Process类属性:
p.name 获取name参数,如果没有,则使用Process-N,N以1开始
p.pid 获取进程id
p.daemon 默认为False,True代表开启守护进程
p.exitcode 获取进程退出状态码,如果进程正在运行则返回None,否则返回退出状态码(正常退出返回0)
p.authkey 获取身份验证32位bytes,是通过os.urandom(32)随机生成的,用于底层身份验证
Process类方法:
p.start() 创建/启动 进程
p.join([timout]) 主进程等待子进程执行结束,并调用wait方法回收子进程资源
p.is_alive() 返回bool值,如果子进程存活返回True
p.terminate() 强行终止子进程
current_process().pid 获取当前进程id
3. 进程间通信(IPC)
默认进程间不可通信,进程间相互隔离,这体现在内存空间上的隔离
不同进程存储各自的全局变量,进程间不会共享全局变量
实现进程间通信有两种方法:
Queue队列
= 管道+锁
管道
Queue类
from multiprocessing import Queue
Queue类按照先入先出算法,对于队列中的数据,先放入的就会被先取出
Queue类的方法
q = Queue([int]) int代表队列长度,不填默认最大
q.qsize() 获取当前队列长度
q.empty() 如果队列位空返回True
q.full() 如果队列为空返回False
q.put(obj, block=True, timeout=None)
q.put_nowait()
q.get(block=True,timeout=None)
q.get_nowait()
参数讲解:
block:设置是否阻塞,False不阻塞,直接报错,put报错queue.Full,get报错queue.Empty
timeout:只有当block=True才有效,值为None代表无限大,一直原地阻塞,可以设置秒数,超过该值则直接报错
xx_nowait() 相当于block=False
JoinableQueue(Queue)继承于Queue类,且新增两个方法
.join()
.task_done()
JoinableQueue类存在一个计数器,当调用一次put方法时计数器+1,当调用一次task_done方法时,计数器-1
当计数器为0时,join方法停止阻塞,否则一直阻塞当前进程
Pipe类
Pipe类的实例化需要在进程创建代码之前,然后作为参数传递给进程对象
a, b =Pipe([duplex])
duplex=True 全双工
duplex=False 半双工
a.send(obj)
b.recv()
a.close()
recv方法会阻塞程序,如果管道中没有数据,则会一直阻塞等待数据
如果管道对端关闭端口,则会抛出EOFError异常
本端端口也要关闭端口
在windows中创建进程,需要将创建进程代码写入__name__语句中
因为,在windows中创建进程,默认会使用import类似的方法将本模块的代码导入到子进程中
因此,如果不加__name__语句,则会不停递归创建进程,从而引发错误
僵尸进程
当子进程运行结束后,会留下一个Zombie僵尸进程的数据结构,该数据结构中包含进程ID、运行时间、退出状态等信息
该Zombie数据结构会一直占用进程资源,只有当父进程调用wait/waitpid方法查看子进程的状态信息时,才会回收进程资源
如果父进程存在循环,从而不能及时调用wait/waitpid方法,或者父进程突然死亡,那么就会一直留下僵尸进程,损耗进程资源
孤儿进程
当父进程创建子进程后,突然死亡,此刻子进程还没有运行完毕,那么这就是一个孤儿进程
在linux中,孤儿进程都会被init进程1收养,init进程会不停循环调用wait方法
因此,当孤儿进程任务执行完毕后,会被立刻回收进程资源
守护进程
守护进程会随着父进程的结束而终结
守护进程不能继续创建子进程
from multiprocessing import Process
p = Process()
p.daemon = True开启守护进程
互斥锁
如果存在多个进程对一个公共资源的访问、修改时,就极容易出现数据错误
此刻就需要加锁处理,将并行变为串行,牺牲效率换取数据安全
from multiprocessing import Lock
mutex = Lock()
mutex.acquire() 加锁
mutex.release() 释放锁