一、背景知识
顾明思义,进程即正在执行的一个过程,进程是对正在云的程序的一个抽象。
进程的概念起源与操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一,操作系统的其他所有内容都是围绕进程的概念展开的。
ps:即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力,将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。
必备的理论基础:
1 #一 操作系统的作用: 2 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 3 2:管理、调度进程,并且将多个进程对硬件的竞争变得有序 4 5 #二 多道技术: 6 1.产生背景:针对单核,实现并发 7 ps: 8 现在的主机一般是多核,那么每个核都会利用多道技术 9 有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个 10 cpu中的任意一个,具体由操作系统调度算法决定。 11 12 2.空间上的复用:如内存中同时有多道程序 13 3.时间上的复用:复用一个cpu的时间片 14 强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样 15 才能保证下次切换回来时,能基于上次切走的位置继续运行
二、python并发编程之进程
1、进程:正在进行的一个过程或者说一个任务,二负责执行任务则是cpu。
举例(单核+多道,实现多个进程的并发执行):
egon在一个时间段内有很多任务要做:python备课的任务,写书的任务,交女朋友的任务,王者荣耀上分的任务,
但egon同一时刻只能做一个任务(cpu同一时间只能干一个活),如何才能玩出多个任务并发执行的效果?
egon备一会课,再去跟李杰的女朋友聊聊天,再去打一会王者荣耀....这就保证了每个任务都在进行中.
2、进程与程序的区别
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。
举例:
想象一位有一手好厨艺的计算机科学家egon正在为他的女儿元昊烘制生日蛋糕。
他有做生日蛋糕的食谱,
厨房里有所需的原料:面粉、鸡蛋、韭菜,蒜泥等。
在这个比喻中:
做蛋糕的食谱就是程序(即用适当形式描述的算法)
计算机科学家就是处理器(cpu)
而做蛋糕的各种原料就是输入数据。
进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。
现在假设计算机科学家egon的儿子alex哭着跑了进来,说:XXXXXXXXXXXXXX。
科学家egon想了想,处理儿子alex蛰伤的任务比给女儿元昊做蛋糕的任务更重要,于是
计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他
离开时的那一步继续做下去。
需要强调的是:同一个程序执行两次,那也是两个进程,比如打开暴风影音,虽然都是同一个软件,但是一个可以播放一个视频。
3、并发与并行
无论是并行还是并发,在用户看来都是‘同时’运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行衣蛾任务。
并发:是伪并行,即看起来是同时运行,单个cpu+多道技术就可以实现并发,(并行也属于并发)
并行:同时运行,只有具备多个cpu才能实现并行
单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术就针对单核而言的)
有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了cpu1,cpu2,cpu3,cpu4,一旦任务1遇到i/o就被迫中断执行,此时任务5就拿到cpu1的时间片去执行,这就是单核下的多道技术,而一旦任务1的i/o结束了,操作系统会重新调用它(需要进程的调度,分配给那个cpu运行,由操作系统说了算),可能被分配给四个cpu中的任意一个去执行
所有现代计算机经常会在同一时间做很多件事,一个用户的pc(无论是单cpu还是多cpu),都可以同时运行多个任务(一个任务可以理解为一个程序)
启动一个进程来杀毒(360软件)
启动一个进程来看电影(暴风影音)
启动一个进程来聊天(腾讯QQ)
所有的这些进程都需被管理,于是一个支持多进程的多道程序系统是至关重要的
多道技术概念回顾:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)
4、同步与异步
同步执行:一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行。
异步执行:一个进程在执行某个任务时,另外一个进程无需等待其执行完毕,就可以继续执行,当有消息返回时,系统会通知进行处理,这样可以提高执行效率。
举个例子,打电话时就是同步通信,发短信时就是异步通信。
5、进程的创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。 而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程 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、进程的状态
tail -f access.log |grep '404' 执行程序tail,开启一个子进程,执行程序grep,开启另外一个子进程,两个进程之间基于管道'|'通讯,将tail的结果作为grep的输入。 进程grep在等待输入(即I/O)时的状态称为阻塞,此时grep命令都无法运行 其实在两种情况下会导致一个进程在逻辑上不能运行, 1. 进程挂起是自身原因,遇到I/O阻塞,便要让出CPU让其他进程去执行,这样保证CPU一直在工作 2. 与进程无关,是操作系统层面,可能会因为一个进程占用时间过多,或者优先级等原因,而调用其他的进程去使用CPU。 因而一个进程由三种状态
9、进程并发的实现
进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),每个进程占用一个进程表项(这些表项也称为进程控制块)
该表存放了进程状态的重要信息:程序计数器,堆栈指针,内存分配状况,所有打开文件的状态,帐号和调度信息,以及其他在进程由运行状态转为就绪态或阻塞态时,必须保存的信息,从而保证该进程在再次启动时,就像从未被中断过一样。
三、python并发编程之进程
1、multiprocessing模块介绍
python中多线程无法利用多核优势,如果想要充分地使用多核cpu的资源(os.cpu_count()),在python中大部分情况需要使用多进程,python提供了multiprocessing。
multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。
multiprocessing模块的功能众多:支持子进程,通信和共享数据,执行不同形式的同步,提供了process、Queue、Lock等组件。
需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限与该进程内。
2、process类的介绍
创建进程的类:
1 Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动) 2 3 强调: 4 1. 需要使用关键字的方式来指定参数 5 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
参数介绍:
1 group参数未使用,值始终为None 2 3 target表示调用对象,即子进程要执行的任务 4 5 args表示调用对象的位置参数元组,args=(1,2,'egon',) 6 7 kwargs表示调用对象的字典,kwargs={'name':'egon','age':18} 8 9 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 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置 2 3 p.name:进程的名称 4 5 p.pid:进程的pid 6 7 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可) 8 9 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
四、process类的使用
注意:在windows中process()必须放到 # if __name__ == '__main__':下
1 Since Windows has no fork, the multiprocessing module starts a new Python process and imports the calling module. 2 If Process() gets called upon import, then this sets off an infinite succession of new processes (or until your machine runs out of resources). 3 This is the reason for hiding calls to Process() inside 4 5 if __name__ == "__main__" 6 since statements inside this if-statement will not get called upon import. 7 由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。 8 如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。 9 这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句中的语句将不会在导入时被调用。 10 11 详细解释
创建并开启子进程的两种方式
1 #开进程的方法一: 2 import time 3 import random 4 from multiprocessing import Process 5 def piao(name): 6 print('%s piaoing' %name) 7 time.sleep(random.randrange(1,5)) 8 print('%s piao end' %name) 9 10 11 12 p1=Process(target=piao,args=('egon',)) #必须加,号 13 p2=Process(target=piao,args=('alex',)) 14 p3=Process(target=piao,args=('wupeqi',)) 15 p4=Process(target=piao,args=('yuanhao',)) 16 17 p1.start() 18 p2.start() 19 p3.start() 20 p4.start() 21 print('主线程') 22 23 方法一
1 #开进程的方法二: 2 import time 3 import random 4 from multiprocessing import Process 5 6 7 class Piao(Process): 8 def __init__(self,name): 9 super().__init__() 10 self.name=name 11 def run(self): 12 print('%s piaoing' %self.name) 13 14 time.sleep(random.randrange(1,5)) 15 print('%s piao end' %self.name) 16 17 p1=Piao('egon') 18 p2=Piao('alex') 19 p3=Piao('wupeiqi') 20 p4=Piao('yuanhao') 21 22 p1.start() #start会自动调用run 23 p2.start() 24 p3.start() 25 p4.start() 26 print('主线程') 27 28 方法二
练习1:socket通信变成并发的形式
1 from socket import * 2 from multiprocessing import Process 3 4 server=socket(AF_INET,SOCK_STREAM) 5 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 6 server.bind(('127.0.0.1',8080)) 7 server.listen(5) 8 9 def talk(conn,client_addr): 10 while True: 11 try: 12 msg=conn.recv(1024) 13 if not msg:break 14 conn.send(msg.upper()) 15 except Exception: 16 break 17 18 if __name__ == '__main__': #windows下start进程一定要写到这下面 19 while True: 20 conn,client_addr=server.accept() 21 p=Process(target=talk,args=(conn,client_addr)) 22 p.start() 23 24 server端
1 from socket import * 2 3 client=socket(AF_INET,SOCK_STREAM) 4 client.connect(('127.0.0.1',8080)) 5 6 7 while True: 8 msg=input('>>: ').strip() 9 if not msg:continue 10 11 client.send(msg.encode('utf-8')) 12 msg=client.recv(1024) 13 print(msg.decode('utf-8')) 14 15 from socket import * 16 17 client=socket(AF_INET,SOCK_STREAM) 18 client.connect(('127.0.0.1',8080)) 19 20 21 while True: 22 msg=input('>>: ').strip() 23 if not msg:continue 24 25 client.send(msg.encode('utf-8')) 26 msg=client.recv(1024) 27 print(msg.decode('utf-8')) 28 29 多个client端
每来一个客户端,都在服务端开启一个进程,如果并发来一个万个客户端,要开启一万个进程吗,你自己尝试着在你自己的机器上开启一万个,10万个进程试一试。
解决方法:进程池
process对象的join方法
1 from multiprocessing import Process 2 import time 3 import random 4 5 class Piao(Process): 6 def __init__(self,name): 7 self.name=name 8 super().__init__() 9 def run(self): 10 print('%s is piaoing' %self.name) 11 time.sleep(random.randrange(1,3)) 12 print('%s is piao end' %self.name) 13 14 15 p=Piao('egon') 16 p.start() 17 p.join(0.0001) #等待p停止,等0.0001秒就不再等了 18 print('开始') 19 20 join:主进程等,等待子进程结束
1 from multiprocessing import Process 2 import time 3 import random 4 def piao(name): 5 print('%s is piaoing' %name) 6 time.sleep(random.randint(1,3)) 7 print('%s is piao end' %name) 8 9 p1=Process(target=piao,args=('egon',)) 10 p2=Process(target=piao,args=('alex',)) 11 p3=Process(target=piao,args=('yuanhao',)) 12 p4=Process(target=piao,args=('wupeiqi',)) 13 14 p1.start() 15 p2.start() 16 p3.start() 17 p4.start() 18 19 #有的同学会有疑问:既然join是等待进程结束,那么我像下面这样写,进程不就又变成串行的了吗? 20 #当然不是了,必须明确:p.join()是让谁等? 21 #很明显p.join()是让主线程等待p的结束,卡住的是主线程而绝非进程p, 22 23 #详细解析如下: 24 #进程只要start就会在开始运行了,所以p1-p4.start()时,系统中已经有四个并发的进程了 25 #而我们p1.join()是在等p1结束,没错p1只要不结束主线程就会一直卡在原地,这也是问题的关键 26 #join是让主线程等,而p1-p4仍然是并发执行的,p1.join的时候,其余p2,p3,p4仍然在运行,等#p1.join结束,可能p2,p3,p4早已经结束了,这样p2.join,p3.join.p4.join直接通过检测,无需等待 27 # 所以4个join花费的总时间仍然是耗费时间最长的那个进程运行的时间 28 p1.join() 29 p2.join() 30 p3.join() 31 p4.join() 32 33 print('主线程') 34 35 36 #上述启动进程与join进程可以简写为 37 # p_l=[p1,p2,p3,p4] 38 # 39 # for p in p_l: 40 # p.start() 41 # 42 # for p in p_l: 43 # p.join() 44 45 有了join,程序不就是串行了吗???
process对象的其他方法或属性
1 #进程对象的其他方法一:terminate,is_alive 2 from multiprocessing import Process 3 import time 4 import random 5 6 class Piao(Process): 7 def __init__(self,name): 8 self.name=name 9 super().__init__() 10 11 def run(self): 12 print('%s is piaoing' %self.name) 13 time.sleep(random.randrange(1,5)) 14 print('%s is piao end' %self.name) 15 16 17 p1=Piao('egon1') 18 p1.start() 19 20 p1.terminate()#关闭进程,不会立即关闭,所以is_alive立刻查看的结果可能还是存活 21 print(p1.is_alive()) #结果为True 22 23 print('开始') 24 print(p1.is_alive()) #结果为False 25 26 terminate与is_alive
1 from multiprocessing import Process 2 import time 3 import random 4 class Piao(Process): 5 def __init__(self,name): 6 # self.name=name 7 # super().__init__() #Process的__init__方法会执行self.name=Piao-1, 8 # #所以加到这里,会覆盖我们的self.name=name 9 10 #为我们开启的进程设置名字的做法 11 super().__init__() 12 self.name=name 13 14 def run(self): 15 print('%s is piaoing' %self.name) 16 time.sleep(random.randrange(1,3)) 17 print('%s is piao end' %self.name) 18 19 p=Piao('egon') 20 p.start() 21 print('开始') 22 print(p.pid) #查看pid 23 24 name与pid
五、守护进程
主进程创建守护进程
主进程创建守护进程
其一:守护进程会在主进程代码执行后结束就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError:daemonic processes are not allowed to have children
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止。
1 from multiprocessing import Process 2 import time 3 import random 4 5 class Piao(Process): 6 def __init__(self,name): 7 self.name=name 8 super().__init__() 9 def run(self): 10 print('%s is piaoing' %self.name) 11 time.sleep(random.randrange(1,3)) 12 print('%s is piao end' %self.name) 13 14 15 p=Piao('egon') 16 p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行 17 p.start() 18 print('主')
1 #主进程代码运行完毕,守护进程就会结束 2 from multiprocessing import Process 3 from threading import Thread 4 import time 5 def foo(): 6 print(123) 7 time.sleep(1) 8 print("end123") 9 10 def bar(): 11 print(456) 12 time.sleep(3) 13 print("end456") 14 15 16 p1=Process(target=foo) 17 p2=Process(target=bar) 18 19 p1.daemon=True 20 p1.start() 21 p2.start() 22 print("main-------") #打印该行则主进程代码结束,则守护进程p1应该被终止,可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止 23 24 迷惑人的例子
六、进程同步(锁)
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题,竞争带来的结果就是错乱,如何控制,就是加锁处理
part1:多个进程共享同一打印终端
1 #并发运行,效率高,但竞争同一打印终端,带来了打印错乱 2 from multiprocessing import Process 3 import os,time 4 def work(): 5 print('%s is running' %os.getpid()) 6 time.sleep(2) 7 print('%s is done' %os.getpid()) 8 9 if __name__ == '__main__': 10 for i in range(3): 11 p=Process(target=work) 12 p.start() 13 14 并发运行,效率高,但竞争同一打印终端,带来了打印错乱
1 #由并发变成了串行,牺牲了运行效率,但避免了竞争 2 from multiprocessing import Process,Lock 3 import os,time 4 def work(lock): 5 lock.acquire() 6 print('%s is running' %os.getpid()) 7 time.sleep(2) 8 print('%s is done' %os.getpid()) 9 lock.release() 10 if __name__ == '__main__': 11 lock=Lock() 12 for i in range(3): 13 p=Process(target=work,args=(lock,)) 14 p.start() 15 16 加锁:由并发变成了串行,牺牲了运行效率,但避免了竞争
part2:多个进程共享同一文件
文件当数据库,模拟抢票
1 #文件db的内容为:{"count":1} 2 #注意一定要用双引号,不然json无法识别 3 from multiprocessing import Process,Lock 4 import time,json,random 5 def search(): 6 dic=json.load(open('db.txt')) 7 print('\033[43m剩余票数%s\033[0m' %dic['count']) 8 9 def get(): 10 dic=json.load(open('db.txt')) 11 time.sleep(0.1) #模拟读数据的网络延迟 12 if dic['count'] >0: 13 dic['count']-=1 14 time.sleep(0.2) #模拟写数据的网络延迟 15 json.dump(dic,open('db.txt','w')) 16 print('\033[43m购票成功\033[0m') 17 18 def task(lock): 19 search() 20 get() 21 if __name__ == '__main__': 22 lock=Lock() 23 for i in range(100): #模拟并发100个客户端抢票 24 p=Process(target=task,args=(lock,)) 25 p.start() 26 27 并发运行,效率高,但竞争写同一文件,数据写入错乱
1 #文件db的内容为:{"count":1} 2 #注意一定要用双引号,不然json无法识别 3 from multiprocessing import Process,Lock 4 import time,json,random 5 def search(): 6 dic=json.load(open('db.txt')) 7 print('\033[43m剩余票数%s\033[0m' %dic['count']) 8 9 def get(): 10 dic=json.load(open('db.txt')) 11 time.sleep(0.1) #模拟读数据的网络延迟 12 if dic['count'] >0: 13 dic['count']-=1 14 time.sleep(0.2) #模拟写数据的网络延迟 15 json.dump(dic,open('db.txt','w')) 16 print('\033[43m购票成功\033[0m') 17 18 def task(lock): 19 search() 20 lock.acquire() 21 get() 22 lock.release() 23 if __name__ == '__main__': 24 lock=Lock() 25 for i in range(100): #模拟并发100个客户端抢票 26 p=Process(target=task,args=(lock,)) 27 p.start() 28 29 加锁:购票行为由并发变成了串行,牺牲了运行效率,但保证了数据安全
总结;
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1、效率低
2、需要自己加锁处理
为此multiprocessing模块为我们提供了基于消息的IPC通信机制:队列和管道。
1、队列和管道都是将数据存放与内存中。
2、队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来。
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
七、队列(推荐使用)
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的
创建队列的类(底层就是以管道和锁定的方式实现):
1 Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
参数介绍:
maxsize是队列中允许最大项数,省略则无大小限制。
方法介绍:
主要方法:
1 q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。 2 q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常. 3 4 q.get_nowait():同q.get(False) 5 q.put_nowait():同q.put(False) 6 7 q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。 8 q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。 9 q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
其他方法:
1 q.cancel_join_thread():不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞 2 q.close():关闭队列,防止队列中加入更多数据。调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。 3 q.join_thread():连接队列的后台线程。此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为
应用:
1 ''' 2 multiprocessing模块支持进程间通信的两种主要形式:管道和队列 3 都是基于消息传递实现的,但是队列接口 4 ''' 5 6 from multiprocessing import Process,Queue 7 import time 8 q=Queue(3) 9 10 11 #put ,get ,put_nowait,get_nowait,full,empty 12 q.put(3) 13 q.put(3) 14 q.put(3) 15 print(q.full()) #满了 16 17 print(q.get()) 18 print(q.get()) 19 print(q.get()) 20 print(q.empty()) #空了
八、生产者消费者模型
生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决大多数并发问题。该模式通过生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据,同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者,为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
基于队列实现生产者消费者模型
1 from multiprocessing import Process,Queue 2 import time,random,os 3 def consumer(q): 4 while True: 5 res=q.get() 6 time.sleep(random.randint(1,3)) 7 print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res)) 8 9 def producer(q): 10 for i in range(10): 11 time.sleep(random.randint(1,3)) 12 res='包子%s' %i 13 q.put(res) 14 print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res)) 15 16 if __name__ == '__main__': 17 q=Queue() 18 #生产者们:即厨师们 19 p1=Process(target=producer,args=(q,)) 20 21 #消费者们:即吃货们 22 c1=Process(target=consumer,args=(q,)) 23 24 #开始 25 p1.start() 26 c1.start() 27 print('主')
此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q后,则一直处于死循环中且卡在q.get()这一步,
解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号就可以break出死循环。
1 from multiprocessing import Process,Queue 2 import time,random,os 3 def consumer(q): 4 while True: 5 res=q.get() 6 if res is None:break #收到结束信号则结束 7 time.sleep(random.randint(1,3)) 8 print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res)) 9 10 def producer(q): 11 for i in range(10): 12 time.sleep(random.randint(1,3)) 13 res='包子%s' %i 14 q.put(res) 15 print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res)) 16 q.put(None) #发送结束信号 17 if __name__ == '__main__': 18 q=Queue() 19 #生产者们:即厨师们 20 p1=Process(target=producer,args=(q,)) 21 22 #消费者们:即吃货们 23 c1=Process(target=consumer,args=(q,)) 24 25 #开始 26 p1.start() 27 c1.start() 28 print('主') 29 30 生产者在生产完毕后发送结束信号None
注意:结束信号None,不一定要由生产者发,主进程同样可以发,但主进程需要等生产者结束后才应该发送该信号。
1 from multiprocessing import Process,Queue 2 import time,random,os 3 def consumer(q): 4 while True: 5 res=q.get() 6 if res is None:break #收到结束信号则结束 7 time.sleep(random.randint(1,3)) 8 print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res)) 9 10 def producer(q): 11 for i in range(2): 12 time.sleep(random.randint(1,3)) 13 res='包子%s' %i 14 q.put(res) 15 print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res)) 16 17 if __name__ == '__main__': 18 q=Queue() 19 #生产者们:即厨师们 20 p1=Process(target=producer,args=(q,)) 21 22 #消费者们:即吃货们 23 c1=Process(target=consumer,args=(q,)) 24 25 #开始 26 p1.start() 27 c1.start() 28 29 p1.join() 30 q.put(None) #发送结束信号 31 print('主') 32 33 主进程在生产者生产完毕后发送结束信号None
但上述解决方式,在有多个生产者和多个消费者时,我们则需要用一个很low的方式去解决。
1 from multiprocessing import Process,Queue 2 import time,random,os 3 def consumer(q): 4 while True: 5 res=q.get() 6 if res is None:break #收到结束信号则结束 7 time.sleep(random.randint(1,3)) 8 print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res)) 9 10 def producer(name,q): 11 for i in range(2): 12 time.sleep(random.randint(1,3)) 13 res='%s%s' %(name,i) 14 q.put(res) 15 print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res)) 16 17 18 19 if __name__ == '__main__': 20 q=Queue() 21 #生产者们:即厨师们 22 p1=Process(target=producer,args=('包子',q)) 23 p2=Process(target=producer,args=('骨头',q)) 24 p3=Process(target=producer,args=('泔水',q)) 25 26 #消费者们:即吃货们 27 c1=Process(target=consumer,args=(q,)) 28 c2=Process(target=consumer,args=(q,)) 29 30 #开始 31 p1.start() 32 p2.start() 33 p3.start() 34 c1.start() 35 36 p1.join() #必须保证生产者全部生产完毕,才应该发送结束信号 37 p2.join() 38 p3.join() 39 q.put(None) #有几个生产者就应该发送几次结束信号None 40 q.put(None) #发送结束信号 41 q.put(None) #发送结束信号 42 print('主') 43 44 有几个生产者就需要发送几次结束信号:相当low
其实我们的思路无非是发送结束信号而已,有另外一种队列提供了这种机制
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()方法为止
1 from multiprocessing import Process,JoinableQueue 2 import time,random,os 3 def consumer(q): 4 while True: 5 res=q.get() 6 time.sleep(random.randint(1,3)) 7 print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res)) 8 9 q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了 10 11 def producer(name,q): 12 for i in range(10): 13 time.sleep(random.randint(1,3)) 14 res='%s%s' %(name,i) 15 q.put(res) 16 print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res)) 17 q.join() 18 19 20 if __name__ == '__main__': 21 q=JoinableQueue() 22 #生产者们:即厨师们 23 p1=Process(target=producer,args=('包子',q)) 24 p2=Process(target=producer,args=('骨头',q)) 25 p3=Process(target=producer,args=('泔水',q)) 26 27 #消费者们:即吃货们 28 c1=Process(target=consumer,args=(q,)) 29 c2=Process(target=consumer,args=(q,)) 30 c1.daemon=True 31 c2.daemon=True 32 33 #开始 34 p_l=[p1,p2,p3,c1,c2] 35 for p in p_l: 36 p.start() 37 38 p1.join() 39 p2.join() 40 p3.join() 41 print('主') 42 43 #主进程等--->p1,p2,p3等---->c1,c2 44 #p1,p2,p3结束了,证明c1,c2肯定全都收完了p1,p2,p3发到队列的数据 45 #因而c1,c2也没有存在的价值了,应该随着主进程的结束而结束,所以设置成守护进程
九、管道
进程间通信(IPC)方式二:管道(不推荐使用,了解即可)
1 #创建管道的类: 2 Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道 3 #参数介绍: 4 dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。 5 #主要方法: 6 conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。 7 conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象 8 #其他方法: 9 conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法 10 conn1.fileno():返回连接使用的整数文件描述符 11 conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。 12 13 conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。 14 conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收 15 16 conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。 17 18 介绍
1 from multiprocessing import Process,Pipe 2 3 import time,os 4 def consumer(p,name): 5 left,right=p 6 left.close() 7 while True: 8 try: 9 baozi=right.recv() 10 print('%s 收到包子:%s' %(name,baozi)) 11 except EOFError: 12 right.close() 13 break 14 def producer(seq,p): 15 left,right=p 16 right.close() 17 for i in seq: 18 left.send(i) 19 # time.sleep(1) 20 else: 21 left.close() 22 if __name__ == '__main__': 23 left,right=Pipe() 24 25 c1=Process(target=consumer,args=((left,right),'c1')) 26 c1.start() 27 28 29 seq=(i for i in range(10)) 30 producer(seq,(left,right)) 31 32 right.close() 33 left.close() 34 35 c1.join() 36 print('主进程') 37 38 基于管道实现进程间通信(与队列的方式是类似的,队列就是管道加锁实现的)
注意:生产者和消费者都没有使用管道的某个端点,就应该将其关闭,如在生产者中关闭管道的右端,在消费者中关闭管道的左端,如果忘记执行这些步骤,程序可能再消费者中的recv()操作上挂起,管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生产EOFError异常,因此在生产者中管道不会有任何效果,付费消费者中也关闭了相同的管道端点。
1 from multiprocessing import Process,Pipe 2 3 import time,os 4 def adder(p,name): 5 server,client=p 6 client.close() 7 while True: 8 try: 9 x,y=server.recv() 10 except EOFError: 11 server.close() 12 break 13 res=x+y 14 server.send(res) 15 print('server done') 16 if __name__ == '__main__': 17 server,client=Pipe() 18 19 c1=Process(target=adder,args=((server,client),'c1')) 20 c1.start() 21 22 server.close() 23 24 client.send((10,20)) 25 print(client.recv()) 26 client.close() 27 28 c1.join() 29 print('主进程') 30 #注意:send()和recv()方法使用pickle模块对对象进行序列化。 31 32 管道可以用于双向通信,利用通常在客户端/服务器中使用的请求/响应模型或远程过程调用,就可以使用管道编写与进程交互的程序
十、共享数据
展望未来,基于消息传递的并发编程是大势所趋
即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合
通过消息队列交换数据,这样极大地减少使用锁定好其他同步手段的需求。
还可以扩展到分布式系统中
进程间通信应该尽量避免使用共享数据的方式
进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的 虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此 A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies. A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array. For example,
1 from multiprocessing import Manager,Process,Lock 2 import os 3 def work(d,lock): 4 # with lock: #不加锁而操作共享的数据,肯定会出现数据错乱 5 d['count']-=1 6 7 if __name__ == '__main__': 8 lock=Lock() 9 with Manager() as m: 10 dic=m.dict({'count':100}) 11 p_l=[] 12 for i in range(100): 13 p=Process(target=work,args=(dic,lock)) 14 p_l.append(p) 15 p.start() 16 for p in p_l: 17 p.join() 18 print(dic) 19 #{'count': 94} 20 21 进程之间操作共享的数据
十一、进程池
在利用python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间,多进程是实现并发的手段之一,需要注意的问题是:
1、很明显需要并发执行的任务通常要远大于核数
2、一个操作系统不可能无限开启进程,通常有几个核就开几个进程
3、进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数数目的进程也无法做到并行)
例如当被操作对象数目不大时,可以直接利用multiprocessing中process动态生成多个进程,司机十几个还好,但如果是上百个,上千个......手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数。
ps:对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。
创建进程池的类:如果指定numprocess为3,则进程池会从无到有创建三个进程,然后自始至终使用这个三个进程去执行所有任务,不会开启其他进程。
Pool([numprocess [,initializer [, initargs]]]):创建进程池
参数介绍:
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值 2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None 3 initargs:是要传给initializer的参数组
方法介绍:
p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async() p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用
其他方法:
1 方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法 2 obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。 3 obj.ready():如果调用完成,返回True 4 obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常 5 obj.wait([timeout]):等待结果变为可用。 6 obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数
应用:
1 from multiprocessing import Pool 2 import os,time 3 def work(n): 4 print('%s run' %os.getpid()) 5 time.sleep(3) 6 return n**2 7 8 if __name__ == '__main__': 9 p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务 10 res_l=[] 11 for i in range(10): 12 res=p.apply(work,args=(i,)) #同步运行,阻塞、直到本次任务执行完毕拿到res 13 res_l.append(res) 14 print(res_l) 15 16 apply同步执行:阻塞式
1 from multiprocessing import Pool 2 import os,time 3 def work(n): 4 print('%s run' %os.getpid()) 5 time.sleep(3) 6 return n**2 7 8 if __name__ == '__main__': 9 p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务 10 res_l=[] 11 for i in range(10): 12 res=p.apply_async(work,args=(i,)) #同步运行,阻塞、直到本次任务执行完毕拿到res 13 res_l.append(res) 14 15 #异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果,否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了 16 p.close() 17 p.join() 18 for res in res_l: 19 print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get 20 21 apply_async异步执行:非阻塞
1 #一:使用进程池(非阻塞,apply_async) 2 #coding: utf-8 3 from multiprocessing import Process,Pool 4 import time 5 6 def func(msg): 7 print( "msg:", msg) 8 time.sleep(1) 9 return msg 10 11 if __name__ == "__main__": 12 pool = Pool(processes = 3) 13 res_l=[] 14 for i in range(10): 15 msg = "hello %d" %(i) 16 res=pool.apply_async(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去 17 res_l.append(res) 18 print("==============================>") #没有后面的join,或get,则程序整体结束,进程池中的任务还没来得及全部执行完也都跟着主进程一起结束了 19 20 pool.close() #关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成 21 pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束 22 23 print(res_l) #看到的是<multiprocessing.pool.ApplyResult object at 0x10357c4e0>对象组成的列表,而非最终的结果,但这一步是在join后执行的,证明结果已经计算完毕,剩下的事情就是调用每个对象下的get方法去获取结果 24 for i in res_l: 25 print(i.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get 26 27 #二:使用进程池(阻塞,apply) 28 #coding: utf-8 29 from multiprocessing import Process,Pool 30 import time 31 32 def func(msg): 33 print( "msg:", msg) 34 time.sleep(0.1) 35 return msg 36 37 if __name__ == "__main__": 38 pool = Pool(processes = 3) 39 res_l=[] 40 for i in range(10): 41 msg = "hello %d" %(i) 42 res=pool.apply(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去 43 res_l.append(res) #同步执行,即执行完一个拿到结果,再去执行另外一个 44 print("==============================>") 45 pool.close() 46 pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束 47 48 print(res_l) #看到的就是最终的结果组成的列表 49 for i in res_l: #apply是同步的,所以直接得到结果,没有get()方法 50 print(i) 51 52 详解:apply_async与apply
练习2:使用进程池维护固定数目的进程
1 #Pool内的进程数默认是cpu核数,假设为4(查看方法os.cpu_count()) 2 #开启6个客户端,会发现2个客户端处于等待状态 3 #在每个进程内查看pid,会发现pid使用为4个,即多个客户端公用4个进程 4 from socket import * 5 from multiprocessing import Pool 6 import os 7 8 server=socket(AF_INET,SOCK_STREAM) 9 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 10 server.bind(('127.0.0.1',8080)) 11 server.listen(5) 12 13 def talk(conn,client_addr): 14 print('进程pid: %s' %os.getpid()) 15 while True: 16 try: 17 msg=conn.recv(1024) 18 if not msg:break 19 conn.send(msg.upper()) 20 except Exception: 21 break 22 23 if __name__ == '__main__': 24 p=Pool() 25 while True: 26 conn,client_addr=server.accept() 27 p.apply_async(talk,args=(conn,client_addr)) 28 # p.apply(talk,args=(conn,client_addr)) #同步的话,则同一时间只有一个客户端能访问 29 30 server端
1 from socket import * 2 3 client=socket(AF_INET,SOCK_STREAM) 4 client.connect(('127.0.0.1',8080)) 5 6 7 while True: 8 msg=input('>>: ').strip() 9 if not msg:continue 10 11 client.send(msg.encode('utf-8')) 12 msg=client.recv(1024) 13 print(msg.decode('utf-8')) 14 15 客户端
发现:并发开启多个客户端,服务端同一时间只有3个不同的pid,干掉一个客户端,另外一个客户端才会进来,被3个进程之一处理。
十二、进程池之回调函数
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了,你可以处理我的结果了,主进程则调用一个函数去处理该结果,该函数即回调函数。
我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O过程,直接拿到的是任务的结果。
1 from multiprocessing import Pool 2 import requests 3 import json 4 import os 5 6 def get_page(url): 7 print('<进程%s> get %s' %(os.getpid(),url)) 8 respone=requests.get(url) 9 if respone.status_code == 200: 10 return {'url':url,'text':respone.text} 11 12 def pasrse_page(res): 13 print('<进程%s> parse %s' %(os.getpid(),res['url'])) 14 parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text'])) 15 with open('db.txt','a') as f: 16 f.write(parse_res) 17 18 19 if __name__ == '__main__': 20 urls=[ 21 'http://www.baidu.com', 22 'http://www.python.org', 23 'http://www.openstack.org', 24 'http://help.github.com/', 25 'http://www.sina.com.cn/' 26 ] 27 28 p=Pool(3) 29 res_l=[] 30 for url in urls: 31 res=p.apply_async(get_page,args=(url,),callback=pasrse_page) 32 res_l.append(res) 33 34 p.close() 35 p.join() 36 print([res.get() for res in res_l]) #拿到的是get_page的结果,其实完全没必要拿该结果,该结果已经传给回调函数处理了 37 38 ''' 39 打印结果: 40 <进程3388> get https://www.baidu.com 41 <进程3389> get https://www.python.org 42 <进程3390> get https://www.openstack.org 43 <进程3388> get https://help.github.com/ 44 <进程3387> parse https://www.baidu.com 45 <进程3389> get http://www.sina.com.cn/ 46 <进程3387> parse https://www.python.org 47 <进程3387> parse https://help.github.com/ 48 <进程3387> parse http://www.sina.com.cn/ 49 <进程3387> parse https://www.openstack.org 50 [{'url': 'https://www.baidu.com', 'text': '<!DOCTYPE html>\r\n...',...}] 51 '''
1 from multiprocessing import Pool 2 import time,random 3 import requests 4 import re 5 6 def get_page(url,pattern): 7 response=requests.get(url) 8 if response.status_code == 200: 9 return (response.text,pattern) 10 11 def parse_page(info): 12 page_content,pattern=info 13 res=re.findall(pattern,page_content) 14 for item in res: 15 dic={ 16 'index':item[0], 17 'title':item[1], 18 'actor':item[2].strip()[3:], 19 'time':item[3][5:], 20 'score':item[4]+item[5] 21 22 } 23 print(dic) 24 if __name__ == '__main__': 25 pattern1=re.compile(r'<dd>.*?board-index.*?>(\d+)<.*?title="(.*?)".*?star.*?>(.*?)<.*?releasetime.*?>(.*?)<.*?integer.*?>(.*?)<.*?fraction.*?>(.*?)<',re.S) 26 27 url_dic={ 28 'http://maoyan.com/board/7':pattern1, 29 } 30 31 p=Pool() 32 res_l=[] 33 for url,pattern in url_dic.items(): 34 res=p.apply_async(get_page,args=(url,pattern),callback=parse_page) 35 res_l.append(res) 36 37 for i in res_l: 38 i.get() 39 40 # res=requests.get('http://maoyan.com/board/7') 41 # print(re.findall(pattern,res.text)) 42 43 爬虫案例
如果在主进程中等待进程池中所有都执行完毕后,再统一处理结果,则无需回调函数
1 from multiprocessing import Pool 2 import time,random,os 3 4 def work(n): 5 time.sleep(1) 6 return n**2 7 if __name__ == '__main__': 8 p=Pool() 9 10 res_l=[] 11 for i in range(10): 12 res=p.apply_async(work,args=(i,)) 13 res_l.append(res) 14 15 p.close() 16 p.join() #等待进程池中所有进程执行完毕 17 18 nums=[] 19 for res in res_l: 20 nums.append(res.get()) #拿到所有结果 21 print(nums) #主进程拿到所有的处理结果,可以在主进程中进行统一进行处理
十三、paramiko模块
1、介绍:
paramiko是一个用于做远程控制的模块,使用该模块可以对远程服务器进行命令或文件操作,值得一说的是,fabric和ansible内部的远程管理就是使用的paramiko来现实。
2、下载安装
pip3 install paramiko #在python3中
1 pycrypto,由于 paramiko 模块内部依赖pycrypto,所以先下载安装pycrypto #在python2中 2 pip3 install pycrypto 3 pip3 install paramiko 4 注:如果在安装pycrypto2.0.1时发生如下错误 5 command 'gcc' failed with exit status 1... 6 可能是缺少python-dev安装包导致 7 如果gcc没有安装,请事先安装gcc 8 9 在python2中
3、使用
sshclient
用于连接远程服务器并执行基本命令
基于用户名密码连接:
1 import paramiko 2 3 # 创建SSH对象 4 ssh = paramiko.SSHClient() 5 # 允许连接不在know_hosts文件中的主机 6 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 7 # 连接服务器 8 ssh.connect(hostname='120.92.84.249', port=22, username='root', password='xxx') 9 10 # 执行命令 11 stdin, stdout, stderr = ssh.exec_command('df') 12 # 获取命令结果 13 result = stdout.read() 14 print(result.decode('utf-8')) 15 # 关闭连接 16 ssh.close()
1 import paramiko 2 3 transport = paramiko.Transport(('120.92.84.249', 22)) 4 transport.connect(username='root', password='xxx') 5 6 ssh = paramiko.SSHClient() 7 ssh._transport = transport 8 9 stdin, stdout, stderr = ssh.exec_command('df') 10 res=stdout.read() 11 print(res.decode('utf-8')) 12 13 transport.close() 14 15 SSHClient 封装 Transport
基于公钥密钥连接:
客户端文件名:id_rsa
服务端必须有文件名:authorized_keys(在用ssh-keygen时,必须制作一个zuthorized_keys,可以用ssh-copy-id来制作)
1 import paramiko 2 3 private_key = paramiko.RSAKey.from_private_key_file('/tmp/id_rsa') 4 5 # 创建SSH对象 6 ssh = paramiko.SSHClient() 7 # 允许连接不在know_hosts文件中的主机 8 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 9 # 连接服务器 10 ssh.connect(hostname='120.92.84.249', port=22, username='root', pkey=private_key) 11 12 # 执行命令 13 stdin, stdout, stderr = ssh.exec_command('df') 14 # 获取命令结果 15 result = stdout.read() 16 print(result.decode('utf-8')) 17 # 关闭连接 18 ssh.close()
1 import paramiko 2 3 private_key = paramiko.RSAKey.from_private_key_file('/tmp/id_rsa') 4 5 transport = paramiko.Transport(('120.92.84.249', 22)) 6 transport.connect(username='root', pkey=private_key) 7 8 ssh = paramiko.SSHClient() 9 ssh._transport = transport 10 11 stdin, stdout, stderr = ssh.exec_command('df') 12 result=stdout.read() 13 print(result.decode('utf-8')) 14 15 transport.close() 16 17 SSHClient 封装 Transport
1 import paramiko 2 from io import StringIO 3 4 key_str = """-----BEGIN RSA PRIVATE KEY----- 5 MIIEoQIBAAKCAQEAsJmFLrSeCumJvga0Gl5O5wVOVwMIy2MpqIyQPi5J87dg89a4 6 Da9fczJog7qoSbRwHFOQoCHNphSlp5KPhGsF6RJewkIw9H1UKV4dCOyl/4HOAkAD 7 rKrsEDmrJ9JlzF2GTTZSnTgVQWcvBS2RKB4eM2R9aJ11xV6X2Hk4YDLTExIWeabb 8 h2TUKw0iyjI8pRuYLKkF2X16u9TBwfOTroGYgiNFHQvhsQppbEbI49NF2XkCkFMi 9 8/7tLjf95InE/VUUq56JqfzyHwdpHou+waXbwtvGgXN3sz+KkuEv6R2qDz06upZV 10 FCZRRpDhzoR8Uh/UEzTGZb8z7FB6EJXUiXJikQIBIwKCAQBBmBuGYFf1bK+BGG7H 11 9ySe81ecqVsJtx4aCFLVRGScWg4RbQKIvXs5an6XU/VdNGQnx0RYvBkvDvuzRRC8 12 J8Bd4kB0CfTtGJuaVigKoQp02HEWx1HSa17+tlWD0c4KFBvwywi+DYQ83S64x8gz 13 eOalX9bPFenqORPUD8R7gJeKvPVc6ZTPeorpuH7u9xayP0Eop8qKxZza9Xh3foVj 14 Qo4IxoYnDN57CIRX5PFSlDDggpmr8FtRF4nAxmFq8LhSp05ivzX/Ku1SNHdaMWZO 15 7va8tISXdLI5m0EGzoVoBvohIbwlxI6kfmamrh6Eas2Jnsc4CLzMsR4jBWt0LHLv 16 /SLnAoGBANaEUf/Jptab9G/xD9W2tw/636i3gLpTPY9KPtCcAxqStNeT6RAWZ5HF 17 lKJg+NKpu3pI45ldAwvts0i+aCZk2xakEWIZWqCmXm31JSPDQTaMGe7H0vOmUaxx 18 ncdpBVdvhMbfFUgei15iKfuafgrKaS9oIkntXEgrC+3wBOI0Gbx3AoGBANLAGxAF 19 TK7ydr+Q1+6/ujs6e8WsXt8HZMa/1khCVSbrf1MgACvZPSSSrDpVwaDTSjlRI4AL 20 bb0l0RFU+/0caMiHilscuJdz9Fdd9Ux4pjROZa3TF5CFhvP7PsZAoxOo+yqJg4zr 21 996GG/aAv4M8lQJ2rDFk/Dgn5y/AaAun1oM3AoGAGIQmoOPYjY4qkHNSRE9lYOl4 22 pZFQilKn8x5tlC8WTC4GCgJGhX7nQ9wQ/J1eQ/YkDfmznH+ok6YjHkGlgLsRuXHW 23 GdcDCwuzBUCWh76LHC1EytUCKnloa3qy8jfjWnMlHgrd3FtDILrC+C7p1Vj2FAvm 24 qVz0moiTpioPL8twp9MCgYEAin49q3EyZFYwxwdpU7/SJuvq750oZq0WVriUINsi 25 A6IR14oOvbqkhb94fhsY12ZGt/N9uosq22H+anms6CicoQicv4fnBHDFI3hCHE9I 26 pgeh50GTJHUA6Xk34V2s/kp5KpThazv6qCw+QubkQExh660SEdSlvoCfPKMCi1EJ 27 TukCgYAZKY1NZ2bjJyyO/dfNvMQ+etUL/9esi+40GUGyJ7SZcazrN9z+DO0yL39g 28 7FT9NMIc2dsmNJQMaGBCDl0AjO1O3b/wqlrNvNBGkanxn2Htn5ajfo+LBU7yHAcV 29 7w4X5HLarXiE1mj0LXFKJhdvFqU53KUQJXBqR6lsMqzsdPwLMJg== 30 -----END RSA PRIVATE KEY-----""" 31 32 private_key = paramiko.RSAKey(file_obj=StringIO(key_str)) 33 transport = paramiko.Transport(('120.92.84.249', 22)) 34 transport.connect(username='root', pkey=private_key) 35 36 ssh = paramiko.SSHClient() 37 ssh._transport = transport 38 39 stdin, stdout, stderr = ssh.exec_command('df') 40 result = stdout.read() 41 print(result.decode('utf-8')) 42 transport.close() 43 44 print(result) 45 46 基于私钥字符串进行连接
sftpclient
用于连接远程服务器并执行上传下载
基于用户名密码上传下载
1 import paramiko 2 3 transport = paramiko.Transport(('120.92.84.249',22)) 4 transport.connect(username='root',password='xxx') 5 6 sftp = paramiko.SFTPClient.from_transport(transport) 7 # 将location.py 上传至服务器 /tmp/test.py 8 sftp.put('/tmp/id_rsa', '/etc/test.rsa') 9 # 将remove_path 下载到本地 local_path 10 sftp.get('remove_path', 'local_path') 11 12 transport.close()
基于公钥密钥上传下载
1 import paramiko 2 3 private_key = paramiko.RSAKey.from_private_key_file('/tmp/id_rsa') 4 5 transport = paramiko.Transport(('120.92.84.249', 22)) 6 transport.connect(username='root', pkey=private_key ) 7 8 sftp = paramiko.SFTPClient.from_transport(transport) 9 # 将location.py 上传至服务器 /tmp/test.py 10 sftp.put('/tmp/id_rsa', '/tmp/a.txt') 11 # 将remove_path 下载到本地 local_path 12 sftp.get('remove_path', 'local_path') 13 14 transport.close()
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import paramiko 4 import uuid 5 6 class Haproxy(object): 7 8 def __init__(self): 9 self.host = '172.16.103.191' 10 self.port = 22 11 self.username = 'root' 12 self.pwd = '123' 13 self.__k = None 14 15 def create_file(self): 16 file_name = str(uuid.uuid4()) 17 with open(file_name,'w') as f: 18 f.write('sb') 19 return file_name 20 21 def run(self): 22 self.connect() 23 self.upload() 24 self.rename() 25 self.close() 26 27 def connect(self): 28 transport = paramiko.Transport((self.host,self.port)) 29 transport.connect(username=self.username,password=self.pwd) 30 self.__transport = transport 31 32 def close(self): 33 34 self.__transport.close() 35 36 def upload(self): 37 # 连接,上传 38 file_name = self.create_file() 39 40 sftp = paramiko.SFTPClient.from_transport(self.__transport) 41 # 将location.py 上传至服务器 /tmp/test.py 42 sftp.put(file_name, '/home/root/tttttttttttt.py') 43 44 def rename(self): 45 46 ssh = paramiko.SSHClient() 47 ssh._transport = self.__transport 48 # 执行命令 49 stdin, stdout, stderr = ssh.exec_command('mv /home/root/tttttttttttt.py /home/root/ooooooooo.py') 50 # 获取命令结果 51 result = stdout.read() 52 53 54 ha = Haproxy() 55 ha.run() 56 57 Demo