并发编程——进程
《关于进程》
1.1什么是进程
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,在早期面向进程设计的计算结构中,进程是程序的基本执行实体,在当代面向线程设计的计算机结构中,进程的容器,程序是指令,数据及其组织形式的描述,进程是程序的实体。
狭义定义:进程是正在运行的程序的实例
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,它是操作系统动态执行的基本单元,在传统的操作系统中,进程是最小的资源分配单元 ,线程是最小的执行单位。
注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱。
注释:进程与进程数据是隔离的。
1.2进程调度
1 先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
1 短作业(进程)优先调度算法(SJ/PF)是指对短作业或短进程优先调度的算法,该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
1 时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。 2 显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。 3 在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。 4 在轮转法中,加入到就绪队列的进程有3种情况: 5 一种是分给它的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。 6 另一种情况是分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。 7 第三种情况就是新创建进程进入就绪队列。 8 如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。
1 前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。 2 而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。 3 (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。 4 (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。 5 6 (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
进程的并行与并发
并行:两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
并发:并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。
区别:并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并发是从宏观上,在一个时间段上可以看出是同时执行段的比如一个服务器同时处理多个session。
同步异步阻塞非阻塞
状态介绍
在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。
(1)就绪状态
当进程已分配到除cpu以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
(2)执行/运行状态当进程已获得处理机,其程序正在处理机执行,此时的进程状态称为执行状态。
(3)阻塞状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的时间可有多种例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等
异步与同步
所谓同步就是一个任务的完成需要依赖另一个任务是,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列,要么成功都成功,失败都失败,两个任务的状态可以保持一致。
所谓异步就是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成不了整个任务就算完成了,至于被依赖的任务最终是否被完成,依赖它的任务无法确定,所以是不可靠的任务序列。
1 # 1.父进程和子进程的启动时异步的 2 # 父进程只负责通知操作系统启动子进程 3 # 接下来的工作由操作系统接手 父进程继续执行 4 # 2.父进程执行完毕之后并不会直接结束程序, 5 # 而是会等待所有的子进程都执行完毕之后才结束 6 # 父进程要负责回收子进程的资源
阻塞与非阻塞
阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的
1 继续上面的那个例子,不论是排队还是使用号码等待通知,如果在这个等待的过程中,等待者除了等待消息通知之外不能做其它的事情,那么该机制就是阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下执行。 2 相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一边等待,这样的状态就是非阻塞的,因为他(等待者)没有阻塞在这个消息通知上,而是一边做自己的事情一边等待。 3 4 注意:同步非阻塞形式实际上是效率低下的,想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有。如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的;而异步非阻塞形式却没有这样的问题,因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换。
同步/异步与阻塞/非阻塞
- 同步阻塞形式
效率最低。拿上面的例子来说,就是你专心排队,什么别的事都不做。
- 异步阻塞形式
如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发(通知)
,也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;
异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。
- 同步非阻塞形式
实际上是效率低下的。
想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换
,效率可想而知是低下的。
- 异步非阻塞形式
效率更高,
因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换
。
比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。
很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表现出来
,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞
。
进程的创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。
进程的结束
1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
2. 出错退出(自愿,python a.py中a.py不存在)
3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)
4. 被其他进程杀死(非自愿,如kill -9)
守护进程
会随着主进程的结束而结束。
主进程创建守护进程
1.守护进程会在主进程代码执行结束后就终止。
2.守护进程内无法再开启子进程,否则抛出异常
1 import os 2 import time 3 from multiprocessing import Process 4 class Myprocess(Process): 5 def __init__(self,person): 6 super().__init__() 7 self.person=person 8 def run(self): 9 print(os.getpid(),self.name) 10 print('%s正和女主播聊天'%self.person) 11 if __name__ == '__main__': 12 13 p=Myprocess('哪吒') 14 p.daemon=True 15 p.start() 16 #time.sleep(10) 17 print('主进程')
关于join
import time import random from multiprocessing import Process def func(index): time.sleep(random.randint(1,3)) print('邮件已经发送完毕') if __name__=='__main__': p=Process(target=func,args=(1,)) p.start() # p.join() #起到一个阻塞的作用让子进程执行完后再执行主进程 print('10个邮件已经发送完毕')
def func(index): time.sleep(random.random()) print('第%s个邮件已经发送完毕'%index) if __name__ == '__main__': p_lst=[] for i in range(10): p=Process(target=func,args=(i,)) p.start() # p.join() p_lst.append(p) for p in p_lst: p.join() print('10个邮件已经发送完毕') # p.join() # print('10个邮件已经发送完毕')
class MyProcess(Process): # def __init__(self,arg): # super().__init__() # self.arg = arg # def run(self): # time.sleep(1) # print('子进程:',os.getpid(),os.getppid(),self.arg) # # if __name__ == '__main__': # # p = MyProcess('参数') # # p.start() # 开启一个子进程,让这个子进程执行run方法 # # p.join() # # print('主进程:',os.getpid()) # for i in range(10): # p = MyProcess('参数%s'%i) # p.start() # 开启一个子进程,让这个子进程执行run方法 # print('主进程:', os.getpid())
多个进程同时运行(注意,子过程的执行顺序不是根据启动顺序决定的)
关于锁——multiprocess.Lock
port time from multiprocessing import Process def func(num,lock): time.sleep(1) print('异步执行',num) lock.acquire() time.sleep(0.5) print('同步执行',num) lock.release() if __name__=='__main__': lock=Lock() for i in range(10): p=Process(target=func,args=(i,lock)) p.start()
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,数据慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:1.效率低(共享数据基于文件,而文件是硬盘上的数据)2.需要自己加锁处理
#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。 队列和管道都是将数据存放于内存中 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性
信息量—multiprocess.Semaphore(了解)
互斥锁同时值允许一个线程更改数据,而信号量Semaphore是同时允许一定数量的线程更改数据。
实现:
信息量同步基于内部计数器,每调用一次acquire(),计数器减一,每调用一次release(),计数器加一,当计数器为0时,acquire()调用被阻塞,这是迪克斯彻信号量概念p()和 v()的python实现。信号量同步机制适用于访问像服务器这样的有限资源。
信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念。简单说就是锁和计数器实现的。
def ktv(person): print('%s走进ktv'%person) time.sleep(random.randint(1,5)) print('%s走出ktv'%person) def func(person,sem): sem.acquire() ktv(person) sem.release() if __name__ == '__main__': sem=Semaphore(2) for i in range(10): p=Process(target=func,args=('person%s'%i,sem)) p.start()
关于事件:
# def traffic_light(e): # print('\033[31m红灯亮\033[0m') # while True: # if e.is_set(): # time.sleep(2) # print('\033[31m红灯亮\033[0m') # e.clear() # else: # time.sleep(2) # print('\033[32m绿灯亮\033[0m') # e.set() # # def car(e,i): # if not e.is_set(): # print('car %s 在等待'%i) # e.wait() # print('car %s 通过了'%i) # # if __name__=='__main__': # e=Event() # p=Process(target=traffic_light,args=(e,)) # p.daemon=True # p.start() # p_lst = [] # for i in range(20): # time.sleep(random.randrange(0, 3, 2)) # p=Process(target=car,args=(e,i)) # p.start() # p_lst.append(p) # for p in p_lst: p.join()
进程间通信——队列和管道(multiprocess.Queue,multiprocess.Pipe)
IPC(inter-Process Communication)
队列
Queue([maxsize]) 创建共享的进程队列。 参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。 底层队列使用管道和锁定实现。
Queue()
创建共享的进程队列,maxsize是队列中允许的最大项数,如果省略次参数,则无大小限制,底层队列使用管道和锁定实现。先实例化一个队列对象,具体方法有以下:
q.get()返回q中一个项目,如果q为空,此方法将阻塞,直到队列有项目可用为止,
q.get_nowait()如果q里面含有项目化和上面get方法一致,如果没有将会报错
q.put()将项目放置队列,如果队列已满,此方法将阻塞至有空间可用为止,
q.put_nowait()没有满会合上方一致,满了将会报错。
q.empty()判断q是否为空。
q.full()判断q是否满了。
q.close()关闭队列,放置队列中加入更多数据,后台线程将继续写入那些已入队列但未写入的数据,但将在此方法完成时马上关闭,如果q被垃圾收集,将自动调用次方法,关闭队列不会再队列使用者中生成任何类型的数据结束信号或异常。
# from multiprocessing import Queue # import queue # q=Queue([2])# 对象规定了只能有两个元素 # q.put(1) # q.put(2) # q.put(3) # print(q.get()) # print(q.get()) #如果取值里面未含有会一直工作不停止。 # print(q.get()) # print(q.get_nowait()) # try: # print(q.get_nowait()) # except queue.Empty: # print('empty')
生产者消费者模型
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
import time import random from multiprocessing import Process,Queue def consumer(q,name): # 处理数据 while True: food = q.get() if food is None:break time.sleep(random.uniform(0.5,1)) print('%s吃了一个%s' % (name, food)) def producer(q,name,food): # 获取数据 for i in range(10): time.sleep(random.uniform(0.3,0.8)) print('%s生产了%s%s'%(name,food,i)) q.put(food+str(i)) if __name__ == '__main__': q = Queue() c1 = Process(target=consumer,args=(q,'alex')) c2 = Process(target=consumer,args=(q,'wusir')) c1.start() c2.start() p1 = Process(target=producer, args=(q, '杨宗河','泔水')) p2 = Process(target=producer, args=(q, '何思浩','鱼刺和骨头')) p1.start() p2.start() p1.join() p2.join() q.put(None) # 有几个consumer就需要放几个None q.put(None)
关于JoinableQueue()
创建可连接的共享进程队列,者就像是一个Queue对象,但队通知列允许项目的使用者通知生产者项目已经被成功处理,通知进程是使用共享的新高和条件变量来实现。
q.task_done()使用者使用此方法发出信号,表示q.get()返回的项目已经被处理,如果调用次方法的次数大于从队列中删除的项目数量,将引发ValueError异常。
q.join()生产者将使用此方法进行阻塞,知道队列中所有项目军备处理,阻塞将会持续到为队列中的每个项目均调用q.task_done()方法为止。
rom multiprocessing import JoinableQueue # JoinableQueue 类 # put # get # task_done 通知队列已经有一个数据被处理了 # q.join() # 阻塞直到放入队列中所有的数据都被处理掉(有多少个数据就接收到了多少taskdone) import time import random from multiprocessing import Process,JoinableQueue def consumer(q,name): # 处理数据 while True: food = q.get() time.sleep(random.uniform(0.5,1)) print('%s吃了一个%s' % (name, food)) q.task_done() def producer(q,name,food): # 获取数据 for i in range(10): time.sleep(random.uniform(0.3,0.8)) print('%s生产了%s%s'%(name,food,i)) q.put(food+str(i)) if __name__ == '__main__': jq = JoinableQueue() c1 = Process(target=consumer,args=(jq,'alex')) c2 = Process(target=consumer,args=(jq,'wusir')) c1.daemon = True c2.daemon = True c1.start() c2.start() p1 = Process(target=producer,args=(jq,'杨宗河','泔水')) p2 = Process(target=producer,args=(jq,'何思浩','鱼刺和骨头')) p1.start() p2.start() p1.join() # 生产者要先把所有的数据都放到队列中 p2.join() jq.join()
管道(了解)
创建管道的类:
Pipe:在进程之间创建一条管道,并返回元组(conn1,conn2),表示管道两端的连接对象,强调:必须在产生Process对象之前产生管道。
参数介绍:1只用于接收,2只能用于发送。
主要方法:conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞,如果连接的另一端已经关闭,那么recv方法会抛出EOFEroor。
conn1.send():通过连接发送对象,obj是与序列化兼容的任意对象
其他方法:
conn1.close():关闭连接,如果conn1被垃圾回收,将自动调用次方法。
注释:另外管道端口要关掉不然会一直持续下去。
# def consumer(left,right): # left.close() # while True: # try: # print(right.recv()) # except EOFError: # break # # if __name__ == '__main__': # left,right = Pipe() # Process(target=consumer,args=(left,right)).start() # right.close() # for i in range(10): # left.send('泔水%s'%i) # left.close() # pipe的端口管理不会随着某一个进程的关闭就关闭 # 操作系统来管理进程对这些端口的使用 # left,right # left,right
进程池和multiprocess.Pool模块