十七、并发编程之多进程
十七、并发编程之多进程
一、多进程
1、什么是进程:
进程就是正在进行的一个过程或者说一个任务,而负责执行任务则是cpu
进程是一种抽象的概念,表示一个执行某件事情的过程,进程的概念起源于操作系统
第一代计算机:程序是固定的,无法修改的,某种计算机只能干某种活
第二代计算机:需要人工参与,将程序攒成一批,统一执行,串行执行,提高计算机的利用率
但是调试麻烦
第三代计算机:为了更好利用计算机资源,产生了多道技术。
多到技术:
1、空间复用:内存分割为多个区域,每个区域存储不同的应用程序
2、时间复用:
1.当一个程序遇到I/O操作时,会切换到其他程序(切换前需要保存当前的执行状态,以便能恢复执行),提高效率
2.当你的应用程序执行时间过长,操作系统会强行切走,以保证其他程序也能正常运行,当然因为CPU速度太快,用户感觉不到,但这样会降低效率
3.有一个优先级更高的任务需要处理,此时也会切走,这也是降低了效率
总结:有了多道技术之后,操作系统可以同时运行多个程序,这种情形称之为并发,但是本质上,这些程序还是一个一个排队执行的
了解知识点:
僵尸进程:一个进程任务执行完就死亡了,但是操作系统不会立即将其清理,为的是开启这个子进程的父进程可以访问到这个子进程的信息
孤儿进程:没有爹的称为孤儿,一个父进程已经死亡,然而它的子进程还在执行着,这时候操作系统会接管这些孤儿进程,孤儿进程无害
二、进程与程序的区别
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程,一个程序正在被操作系统读取并执行,就变成了进程
需要强调的是:同一个程序执行两次,那也是两个进程,比如打开暴风影音,虽然都是同一个软件,但是一个可以播放苍井空,一个可以播放饭岛爱
启动进程的方式:
1.系统初始化,会产生一个根进程
2.用户的交互请求,鼠标双击某个程序
3.在一个进程中,发起了系统调用启动了另一个进程
4.批处理作业开始,某些专用的计算机可能还在使用,一般我们用不到
不同操作系统创建进程的方式不同:
unix:完全拷贝父进程的所有数据,子进程不可以访问父进程的数据,但是可以访问拷贝过来的数据副本
windows:创建子进程,加载父进程中所有可执行的文件
三、并发与并行
无论是并行还是并发,在用户看来都是“同时”运行的,不管是进程还是线程,都只是一个任务而已,真实干活的是CPU,CPU来做这些任务,而一个CPU同一时刻只能执行一个任务
1、并发:是伪并行,即看起来是同时运行的,单个CPU+多道技术就可以实现并发,(并行也属于并发)
2、并行:同时运行,只有具备多个CPU才能实现并行
单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的)
有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了CPU1,CPU2,CPU3,CPU4,一旦任务1遇到I/O就被迫中止执行,此时任务5就拿到CPU1的时间片去执行,这就是单核下的多道技术,而一旦任务1的I/O结束了,操作系统会重新调用它(需知进程的调度,分配给哪个CPU运行,由操作系统说了算),可能被分配给四个CPU中的任意一个去执行
四、Process类的介绍
1 Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动) 2 3 强调: 4 1. 需要使用关键字的方式来指定参数 5 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
1 #参数介绍 2 group参数未使用,值始终为None 3 4 target表示调用对象,即子进程要执行的任务 5 6 args表示调用对象的位置参数元组,args=(1,2,3,) 7 8 kwargs表示调用对象的字典,kwargs={'name':'maple','age':18} 9 10 name为子进程的名称
#方法介绍
1 p.start():启动进程,并调用该子进程中的p.run() 2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 3 4 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁 5 p.is_alive():如果p仍然运行,返回True 6 7 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
1 #属性介绍 2 3 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置 4 5 p.name:进程的名称 6 7 p.pid:进程的pid 8 9 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可) 10 11 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
五、开启子进程的方式
方式一
1 import time 2 from multiprocessing import Process 3 import os 4 5 def task(i): 6 print('开始%s'%i) 7 time.sleep(3) 8 print('这是子进程的内容....%s'%i) 9 print('结束%s'%i) 10 11 12 if __name__ == '__main__': 13 start=time.time() 14 for i in range(1,10): 15 #创建子进程 16 p=Process(target=task,args=(i,)) 17 #启动进子程 18 p.start() 19 #默认等待子进程执行结束,可以设置等待时间 20 p.join() 21 print('%s'%(time.time()-start)) 22 print('子进程ID%s,当前执行进程ID%s,当前执行进程的父进程ID%s'%(p.pid,os.getpid(),os.getppid()))
方式二
1 from multiprocessing import Process 2 import time 3 4 class MyProcess(Process): 5 def __init__(self,data): 6 super().__init__() 7 self.data=data 8 def run(self,): 9 print('开始子进程%s'%self.data) 10 time.sleep(3) 11 print('这是子进程内的程序%s'%self.data) 12 13 if __name__ == '__main__': 14 for data in range(1,10): 15 p=MyProcess(data) 16 p.start() 17 p.join() 18 print('这是主进程的程序')
六、守护进程
1、守护就是一个进程陪伴着另一个进程
在代码中,进程只能由进程类守护,一个进程守护者另一个进程,指的是两个进程之间的关联关系
特点:当被守护进程的进程 死亡时,守护进程会跟随被守护进程一起死亡
守护进程怎么创建的:
主进程创建守护进程
1.守护进程会在主进程代码执行结束后就终止
2.守护进程内无法再开启子进程,否则抛出异常
注意点:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
1 from multiprocessing import Process 2 import time 3 4 def task(): 5 print("妃子 升级为皇后") 6 time.sleep(3) 7 print("皇后 挂了") 8 9 if __name__ == '__main__': 10 p = Process(target=task) 11 # 将这个子进程设置为当前进程守护进程 12 p.daemon = True 13 p.start() 14 print("崇祯登基") 15 print("崇祯驾崩了....")
七、互斥锁
1、进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件是没有问题的,而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理
所以互斥锁就是一个bool类型的标识符,多进程在执行任务之前先判断标识符,这样进程之间就会相互排斥
本质处理的方法:把进程的并发改为串行,但这样程序的执行速度就慢了,牺牲了速度保证了数据安全
2、对于竞争的解决方式:方式一:join(不使用,失去了多进程的意思)
方式二:Lock加锁
1 # 解决方法一: 2 # 用join把并发的多进程改为串行 3 # 相同点:都是把并发改串行 4 # 缺点:加上join会导致进程执行顺序的不公平 5 6 from multiprocessing import Process 7 8 def task1(): 9 for i in range(10000): 10 print('=====') 11 12 def task2(): 13 for i in range(10000): 14 print('===================') 15 16 def task3(lock): 17 for i in range(10000): print('=================================') 18 if __name__ == '__main__': 19 p1=Process(target=task1,args=(locking,)) 20 p2 = Process(target=task2, args=(locking,)) 21 p3 = Process(target=task3, args=(locking,)) 22 p1.start() 23 p1.join() 24 p2.start() 25 p2.join() 26 p3.start() 27 p3.join()
1 # 解决方法二: 2 # 加互斥锁 3 # 相同点:都是把并发改串行 4 # 优点:多进程谁先抢到资源谁先处理 5 # 可以指定代码哪些代码要串行 6 # 注意点:要想锁住资源必须保证,多进程拿到的是同一把锁 7 8 from multiprocessing import Process,Lock 9 10 def task1(lock): 11 lock.acquire() 12 for i in range(10000): 13 print('=====') 14 lock.release() 15 def task2(lock): 16 lock.acquire() 17 for i in range(10000): 18 print('===================') 19 lock.release() 20 21 def task3(lock): 22 lock.acquire() 23 for i in range(10000): 24 print('=================================') 25 lock.release() 26 if __name__ == '__main__': 27 locking = Lock() 28 p1=Process(target=task1,args=(locking,)) 29 p2 = Process(target=task2, args=(locking,)) 30 p3 = Process(target=task3, args=(locking,)) 31 p1.start() 32 p2.start() 33 p3.start()
八、IPC(进程间通信)
1、IPC指的是进程间通信,之所以开启子进程,肯定需要它帮我们完成任务,很多情况下需要将数据返回给父进程,然而进程内存是物理隔离的
解决方案:
1.将共享数据放到文件中,但操作文件就是操作I/O,I/O越来速度越慢
2.管道subprocess中的那个管道只能单向通讯,必须存在父子关系
3.共享一块内存区域,需要操作系统帮你分配,优点就是速度快
操作共享内存的二种方式:
1 #Manager函数 2 from multiprocessing import Process,Manager 3 import time 4 5 def task1(dic): 6 dic['name']='maple' 7 print(dic) 8 9 10 if __name__ == '__main__': 11 m=Manager() 12 dic=m.dict({}) 13 p1=Process(target=task1,args=(dic,)) 14 p1.start() 15 #必须添加下面三种方式的代码,等待子进程执行结束,不然就抛出异常 16 # 1 p1.join() 17 # 2 time.sleep(1) 18 # 3 for i in range(100000): 19 # print('%s:====='%i) 20 print(dic)
1 #Queue类 2 from multiprocessing import Process,Queue 3 4 def task1(q): 5 q.put('hello') 6 q.put(12345) 7 q.put({'name':'maple'}) 8 9 if __name__ == '__main__': 10 q=Queue(5) 11 p1=Process(target=task1,args=(q,)) 12 p1.start() 13 14 print(q.get()) 15 print(q.get()) 16 print(q.get())
1 #Queue的一些常用方法: 2 3 1 Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。 4 5 2 maxsize是队列中允许最大项数,省略则无大小限制。 6 7 q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。 8 q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常. 9 10 q.get_nowait():同q.get(False) 11 q.put_nowait():同q.put(False) 12 13 q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。 14 q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。 15 q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样 16 17 其他方法(了解): 18 1 q.cancel_join_thread():不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞 19 2 q.close():关闭队列,防止队列中加入更多数据。调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。 20 3 q.join_thread():连接队列的后台线程。此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为
九、生产者消费者模型
1、生产者消费者模型也是一种编程的套路与思想
在并发编程中使用生产者和消费者模型能够解决绝大多数并发的问题,该模式通过平衡生产进程和消费进程的工作能力来提高程序的整体处理数据速度
2、为什么要使用生产者消费者模型
在进程世界里,生产者就是生产数据的进程,消费者就是消费数据的进程,在多进程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据,同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者,为了解决这个问题于是引入了生产者和消费者模型
3、什么是生产者消费者模型
生产者消费者模型是通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接人给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
1 import time, random 2 from multiprocessing import Process, JoinableQueue 3 4 5 def make_coffee(name, q): 6 for i in range(1, 5): 7 time.sleep(random.randint(1, 3)) 8 print('%s生产了一杯咖啡 %s' % (name, i)) 9 data = '%s生产的咖啡%s' % (name, i) 10 q.put(data) 11 12 13 def drink_coffee(q, name): 14 while True: 15 data = q.get() 16 time.sleep(random.randint(1, 3)) 17 print('%s喝了%s ' % (name, data)) 18 q.task_done() 19 20 21 if __name__ == '__main__': 22 q = JoinableQueue() 23 p1 = Process(target=make_coffee, args=("店主1", q)) 24 p2 = Process(target=make_coffee, args=("店主2", q)) 25 p3 = Process(target=make_coffee, args=("店主3", q)) 26 c1 = Process(target=drink_coffee, args=(q, 'maple')) 27 c2 = Process(target=drink_coffee, args=(q, 'ffm')) 28 p1.start() 29 p2.start() 30 p3.start() 31 c1.daemon=True 32 c2.daemon=True 33 c1.start() 34 c2.start() 35 36 p1.join() 37 p2.join() 38 p3.join() 39 40 q.join()
1 #JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。 2 3 #参数介绍: 4 maxsize是队列中允许最大项数,省略则无大小限制。 5 #方法介绍: 6 JoinableQueue的实例p除了与Queue对象相同的方法之外还具有: 7 q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常 8 q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
十、进程池
在利用python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间,多进程是实现并发的手段之一,需要注意的问题是:
1.很明显需要并发执行的任务通常要远大于核数
2.一个操作系统不可能无限开启进程,通常有几个核就开几个进程
3.进程开启过多,效率反而会下降,开启进程是需要占用系统资源的,而且开启多余核数的进程也无法做到并行
#进程池
1 from threading import current_thread 2 from concurrent.futures import ProcessPoolExecutor 3 import time,random,os 4 5 def work(): 6 time.sleep(random.randint(1,3)) 7 #一个进程对应一个主线程 8 print(os.getpid(),current_thread()) 9 10 11 def run(): 12 #默认为os.cpu_count()数 13 print(os.cpu_count()) 14 pool=ProcessPoolExecutor() 15 for i in range(30): 16 pool.submit(work) 17 18 if __name__ == '__main__': 19 run()
1 #线程池 2 from threading import current_thread 3 from concurrent.futures import ThreadPoolExecutor 4 import time,random,os 5 6 def work(): 7 time.sleep(random.randint(1,3)) 8 #一个进程对应多个线程 9 print(os.getpid(),current_thread()) 10 11 12 def run(): 13 #默认为os.cpu_count()数*5 14 print(os.cpu_count()) 15 pool=ThreadPoolExecutor() 16 for i in range(30): 17 pool.submit(work) 18 19 if __name__ == '__main__': 20 run()
1 #多进程内开多线程 2 3 from threading import current_thread 4 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 5 import time,random,os 6 7 start=time.time() 8 def work1(): 9 num=1 10 for i in range(1,100000): 11 num+=1 12 print(num) 13 14 15 def work(): 16 time.sleep(random.randint(1,3)) 17 #一个进程对应一个主线程 18 print(os.getpid(),current_thread()) 19 pool=ThreadPoolExecutor() 20 for i in range(30): 21 pool.submit(work1) 22 23 24 def run(): 25 #默认为os.cpu_count()数 26 print(os.cpu_count()) 27 pool=ProcessPoolExecutor() 28 for i in range(30): 29 pool.submit(work) 30 31 if __name__ == '__main__': 32 run()
print(time.time()-start)
1 from concurrent.futures import ProcessPoolExecutor 2 3 pool=ProcessPoolExecutor() 4 Future=pool.submit() 5 6 #用cancel(),可以终止某个线程和进程的任务,返回状态为 True False 7 Future.cancel() 8 9 #判断是否真的结束了任务。 10 Future.cancelled() 11 12 #判断是否还在运行 13 Future.running() 14 15 #判断是正常执行完毕的。 16 Future.done() 17 18 #针对result结果做超时的控制。 19 Future.result(timeout=None)
2、回调函数
方法:map,submit,add_done_callback,result
进程池中的回调函数是父进程调用的,和子进程没有关系
线程池中的回调函数是子线程调用的,和父线程没有关系
1 需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数 2 3 我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。
1 # 进程池回调函数 2 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 3 import os 4 5 6 def func(num): 7 info = '这是我的第%s' % num 8 return info 9 10 def call_back_func(res): 11 print(res.result()) 12 13 if __name__ == '__main__': 14 p = ProcessPoolExecutor(20) 15 for i in range(1000): 16 p.submit(func,i).add_done_callback(call_back_func) 17 p.shutdown() 18 19 # 线程池回调函数 20 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 21 import os 22 23 def func(num): 24 info = '这是我的第%s' % num 25 return info 26 27 def call_back_func(res): 28 print(res.result()) 29 30 if __name__ == '__main__': 31 p = ThreadPoolExecutor(20) 32 for i in range(1000): 33 p.submit(func,i).add_done_callback(call_back_func) 34 p.shutdown()
1 #shutdown(wait=True) 2 相当于进程池的pool.close()+pool.join()操作 3 wait=True,等待池内所有任务执行完毕回收完资源后才继续 4 wait=False,立即返回,并不会等待池内的任务执行完毕 5 但不管wait参数为何值,整个程序都会等到所有任务执行完毕