并发编程之多进程
一. 简介
1.1 什么是进程
进程:正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。
1.2 并发与并行
无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务
1.3 同步\异步and阻塞\非阻塞
同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。
阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程
同步:
#所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,
特指那些需要其他部件协作或者需要一定时间完成的任务。 #举例: #1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束 #2. concurrent.futures.ProcessPoolExecutor().submit(func,).result() #3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
异步:
#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,
那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,
效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。 #举例: #1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。 #2. concurrent.futures.ProcessPoolExecutor(3).submit(func,) #3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
阻塞:
#阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。
对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 #举例: #1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态); #2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
再通过这个图帮助理解同步调用和阻塞调用:
二 . python中的多进程
2.1 multiprocessing模块介绍
python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程。Python提供了multiprocessing。
multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。
multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。
2.2 开启子进程的两种方式
#方式一: from multiprocessing import Process import time def task(name): print('%s is running' %name) time.sleep(1) print('%s is done' %name) if __name__ == '__main__': # Process(target=task,kwargs={'name':'子进程1'}) p=Process(target=task,args=('子进程1',)) # p.daemon = True p.start() #仅仅只是给操作系统发送了一个信号 print('主')
# 方法二:
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
print('%s is running' %self.name)
time.sleep(1)
print('%s is done' %self.name)
if __name__ == '__main__':
p=MyProcess('子进程1')
p.start()
# p.join() # 主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。
print('主')
2.3 查看pid
from multiprocessing import Process import time,os def task(): print('%s is running,parent id is <%s>' %(os.getpid(),os.getppid())) time.sleep(1) print('%s is done,parent id is <%s>' %(os.getpid(),os.getppid())) if __name__ == '__main__': p=Process(target=task,) p.start() print('主',os.getpid(),os.getppid())
结果:
主 768 10604 12440 is running,parent id is <768> 12440 is done,parent id is <768>
2.4 Process对象的其他方法
2.4.1 join方法
注意:join方法是让主进程等,等待子进程的结束。
# join方法
from multiprocessing import Process
import time,os
def task():
print('%s is running,parent id is <%s>' %(os.getpid(),os.getppid()))
time.sleep(1)
print('%s is done,parent id is <%s>' %(os.getpid(),os.getppid()))
if __name__ == '__main__':
p=Process(target=task,)
p.start()
p.join()
print('主',os.getpid(),os.getppid())
time.sleep(1)
print(p.pid, p.is_alive())
结果:
11520 is running,parent id is <11244> 11520 is done,parent id is <11244> 主 11244 10604 11520 False
多个进程可以循环调用join方法:
def task(name,n): print('%s is running' %name) time.sleep(n) if __name__ == '__main__': start=time.time() p1=Process(target=task,args=('子进程1',5)) p2=Process(target=task,args=('子进程2',3)) p3=Process(target=task,args=('子进程3',2)) p_l=[p1,p2,p3] # p1.start() # p2.start() # p3.start() for p in p_l: p.start() # p1.join() # p2.join() # p3.join() for p in p_l: p.join() print('主',(time.time()-start))
其他两个方法:terminate和is_alive
#进程对象的其他方法一:terminate,is_alive from multiprocessing import Process import time import random class Piao(Process): def __init__(self,name): self.name=name super().__init__() def run(self): print('%s is piaoing' %self.name) time.sleep(random.randrange(1,5)) print('%s is piao end' %self.name) p1=Piao('egon1') p1.start() p1.terminate()#关闭进程,不会立即关闭,所以is_alive立刻查看的结果可能还是存活 print(p1.is_alive()) #结果为True print('开始') print(p1.is_alive()) #结果为False
2.5 守护进程
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
from multiprocessing import Process import time import random class Piao(Process): def __init__(self,name): self.name=name super().__init__() def run(self): print('%s is piaoing' %self.name) time.sleep(random.randrange(1,3)) print('%s is piao end' %self.name) p=Piao('egon') p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行 p.start() print('主')
# 结果只会打印一个"主", 整个进程就结束了。。。
在看一个迷惑人的例子:
from multiprocessing import Process import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': p1=Process(target=foo) p2=Process(target=bar) p1.daemon=True p1.start() p2.start() print("main-------")
结果:打印该行则主进程代码结束,则守护进程p1应该被终止,可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止
main-------
456
end456
2.6 互斥锁
from multiprocessing import Process,Lock import time # # # def task(name, mutex): mutex.acquire() print('%s 1' %name) time.sleep(1) print('%s 2' %name) time.sleep(1) print('%s 3' %name) mutex.release() if __name__ == '__main__': mutex = Lock() for i in range(3): p=Process(target=task,args=('进程%s' %i,mutex)) p.start() print("主:")
结果:是有序的。
主: 进程0 1 进程0 2 进程0 3 进程1 1 进程1 2 进程1 3 进程2 1 进程2 2 进程2 3
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理
因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
2.6.1 模拟抢票
这个db.json是一个{"count":1}
from multiprocessing import Process,Lock import json import time def search(name): time.sleep(1) dic=json.load(open('db.json','r',encoding='utf-8')) print('<%s> 查看到剩余票数【%s】' %(name,dic['count'])) def get(name): # time.sleep(1) dic=json.load(open('db.json','r',encoding='utf-8')) if dic['count'] > 0: dic['count']-=1 time.sleep(1) json.dump(dic,open('db.json','w',encoding='utf-8')) print('<%s> 购票成功' %name) def task(name,mutex): search(name) mutex.acquire() get(name) mutex.release() if __name__ == '__main__': mutex=Lock() for i in range(10): p=Process(target=task,args=('路人%s' %i,mutex)) p.start()
结果:
<路人2> 查看到剩余票数【1】 <路人1> 查看到剩余票数【1】 <路人4> 查看到剩余票数【1】 <路人3> 查看到剩余票数【1】 <路人7> 查看到剩余票数【1】 <路人8> 查看到剩余票数【1】 <路人0> 查看到剩余票数【1】 <路人5> 查看到剩余票数【1】 <路人6> 查看到剩余票数【1】 <路人9> 查看到剩余票数【1】 <路人2> 购票成功
2.7 队列
简单使用
from multiprocessing import Queue q=Queue(3) q.put('hello') q.put({'a':1}) q.put([3,3,3,]) print(q.full()) # q.put(4) print(q.get()) print(q.get()) print(q.get()) print(q.empty()) print(q.get())
其他方法:
q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,
该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,
会立即抛出Queue.Full异常。
q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,
那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,
如果队列为空,则立即抛出Queue.Empty异常.
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
2.8 生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式?
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式?
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
基于队列实现生产者消费者模型:
from multiprocessing import Process,Queue import time def producer(q): for i in range(3): res='包子%s' %i time.sleep(0.5) print('生产者生产了%s' %res) q.put(res) def consumer(q): while True: res=q.get() if res is None:break time.sleep(1) print('消费者吃了%s' % res) if __name__ == '__main__': #容器 q=Queue() #生产者们 p1=Process(target=producer,args=(q,)) p2=Process(target=producer,args=(q,)) p3=Process(target=producer,args=(q,)) #消费者们 c1=Process(target=consumer,args=(q,)) c2=Process(target=consumer,args=(q,)) p1.start() p2.start() p3.start() c1.start() c2.start() p1.join() # 等待生产者都结束了,才可以发送None哦 p2.join() p3.join() q.put(None) # 生产者在生产完毕后发送结束信号None q.put(None) print('主')
结果:
生产者生产了包子0
生产者生产了包子0
生产者生产了包子0
生产者生产了包子1
生产者生产了包子1
生产者生产了包子1
消费者吃了包子0
生产者生产了包子2
消费者吃了包子0
生产者生产了包子2
生产者生产了包子2
主
消费者吃了包子0
消费者吃了包子1
消费者吃了包子1
消费者吃了包子1
消费者吃了包子2
消费者吃了包子2
消费者吃了包子2
但上述解决方式,在有多个生产者和多个消费者时,我们则需要用一个很low的方式去解决,有多少个消费者就要发送多少个None,所有引出Joinablequeue队列。
2. 9 Joinablequeue队列
这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
参数:
#参数介绍: maxsize是队列中允许最大项数,省略则无大小限制。 #方法介绍: JoinableQueue的实例p除了与Queue对象相同的方法之外还具有: q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常 q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
举例:
from multiprocessing import Process,JoinableQueue import time def producer(q): for i in range(2): res='包子%s' %i time.sleep(0.5) print('生产者生产了%s' %res) q.put(res) q.join() def consumer(q): while True: res=q.get() time.sleep(2) print('消费者吃了%s' % res) q.task_done() if __name__ == '__main__': #容器 q=JoinableQueue() #生产者们 p1=Process(target=producer,args=(q,)) p2=Process(target=producer,args=(q,)) p3=Process(target=producer,args=(q,)) #消费者们 c1=Process(target=consumer,args=(q,)) c2=Process(target=consumer,args=(q,)) c1.daemon=True c2.daemon=True # 如果不写成守护进程,那么消费者就不知道队列是否已经为空了 p1.start() p2.start() p3.start() c1.start() c2.start() p1.join() p2.join() p3.join() print('主')
2.10 信号量
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去,如果指定信号量为3,那么来一个人获得一把锁,计数加1,当计数等于3时,后面的人均需要等待。一旦释放,就有人可以获得一把锁
from multiprocessing import Process,Semaphore import time,random def go_wc(sem,user): sem.acquire() print('%s 占到一个茅坑' %user) time.sleep(random.randint(0,3)) #模拟每个人拉屎速度不一样,0代表有的人蹲下就起来了 sem.release() if __name__ == '__main__': sem=Semaphore(5) p_l=[] for i in range(13): p=Process(target=go_wc,args=(sem,'user%s' %i,)) p.start() p_l.append(p) for i in p_l: i.join() print('============》')
2.11 事件
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
clear:将“Flag”设置为False
set:将“Flag”设置为True
from multiprocessing import Process,Event import time,random def police_car(e,n): while True: if not e.is_set(): print('\033[31m红灯亮\033[0m,car%s等着' % n) e.wait(1) print('灯的是%s,警车走了,car %s' %(e.is_set(),n)) break def traffic_lights(e,inverval): while True: time.sleep(inverval) if e.is_set(): e.clear() #e.is_set() ---->False else: e.set() if __name__ == '__main__': e=Event() for i in range(5): p = Process(target=police_car, args=(e, i,)) p.start() t=Process(target=traffic_lights,args=(e,10)) t.start() print('============》')
2.12 进程池Pool
创建进程池的类:如果指定numprocess为3,则进程池会从无到有创建三个进程,然后自始至终使用这三个进程去执行所有任务,不会开启其他进程
Pool([numprocess [,initializer [, initargs]]]):创建进程池
主要方法:
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()之后调用
from multiprocessing import Pool import os, time def work(n): print("%s is runnung" % os.getpid()) time.sleep(0.1) return n**2 if __name__ == '__main__': pool = Pool(3) res_l = [] for i in range(10): res = pool.apply(work, args=(i,)) res_l.append(res) print("========================") """ #同步调用,直到本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞,但不管该任务是否存在阻塞, 同步调用都会在原地等着,只是等的过程中若是任务发生了阻塞就会被夺走cpu的执行权限 """ print(res_l)
结果:
14592 is runnung 11640 is runnung 14688 is runnung 14592 is runnung 11640 is runnung 14688 is runnung 14592 is runnung 11640 is runnung 14688 is runnung 14592 is runnung ======================== # 注意同步调用会等待任务全部执行完毕,才会继续打印这一行哦 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
异步调用:
def work(n): print("%s is runnung" % os.getpid()) time.sleep(0.1) return n**2 if __name__ == '__main__': pool = Pool(3) res_l = [] for i in range(10): res = pool.apply_async(work, args=(i,)) res_l.append(res) print("========================") """ #异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果, 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了 #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get """ pool.close() pool.join() for res in res_l: print(res.get())
结果:
======================== # 异步调用,提交完任务后,会继续执行自己的代码。 12760 is runnung 8196 is runnung 13432 is runnung 13432 is runnung 8196 is runnung 12760 is runnung 13432 is runnung 8196 is runnung 12760 is runnung 8196 is runnung 0 1 4 9 16 25 36 49 64 81
通过进程池来实现简单的socket通信:
服务端:
import socket from multiprocessing import Pool import os server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9527)) server.listen(5) def talk(conn,client_addr): print('进程pid: %s' % os.getpid()) while True: try: msg = conn.recv(1024) if not msg: break conn.send(msg.upper()) except Exception: break if __name__ == '__main__': pool = Pool(3) while True: conn, addr = server.accept() pool.apply_async(talk, args=(conn, addr,))
客户端:
import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect_ex(('127.0.0.1', 9527)) while True: msg=input('>>: ').strip() if not msg:continue client.send(msg.encode('utf-8')) msg=client.recv(1024) print(msg.decode('utf-8'))
回调函数:
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数
我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。
from multiprocessing import Pool import requests import json import os def get_page(url): print('<进程%s> get %s' %(os.getpid(),url)) respone=requests.get(url) if respone.status_code == 200: return {'url':url,'text':respone.text} def pasrse_page(res): print('<进程%s> parse %s' %(os.getpid(),res['url'])) parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text'])) with open('db.txt','a') as f: f.write(parse_res) if __name__ == '__main__': urls=[ 'https://www.baidu.com', 'https://www.python.org', ] p=Pool(3) res_l=[] for url in urls: res=p.apply_async(get_page,args=(url,),callback=pasrse_page) res_l.append(res) p.close() p.join()
2.13 concurrent.futures的进程池
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor import os,time,random def task(name): print('name:%s pid:%s run' %(name,os.getpid())) time.sleep(random.randint(1,3)) if __name__ == '__main__': pool=ProcessPoolExecutor(4) # pool=ThreadPoolExecutor(5) for i in range(10): pool.submit(task,'egon%s' %i) pool.shutdown(wait=True) # 这个shutdown意思是:入口已经关闭了,拿到了一个准确的线程数,等到这个池子里的所有进程或者线程都执行完毕后,
#才继续执行下面的代码。 print('主')
官网:https://docs.python.org/zh-cn/dev/library/concurrent.futures.html