并发编程之多进程3 (生产者与消费者模型) 回调函数
生产消费者模型
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
实例1:
from multiprocessing import Process,Queue import time,random,os def procducer(q): for i in range(10): res='包子%s' %i time.sleep(0.5) q.put(res) print('%s 生产了 %s' %(os.getpid(),res)) def consumer(q): while True: res=q.get() if res is None: break print('%s 吃 %s' %(os.getpid(),res)) time.sleep(random.randint(2,3)) if __name__ == '__main__': q=Queue() p=Process(target=procducer,args=(q,)) c=Process(target=consumer,args=(q,)) p.start() c.start() print('主')
此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。
例子2:
from multiprocessing import Process,Queue import time,random,os def procducer(q): for i in range(10): res='包子%s' %i time.sleep(0.5) q.put(res) print('%s 生产了 %s' %(os.getpid(),res)) def consumer(q): while True: res=q.get() if res is None: break print('%s 吃 %s' %(os.getpid(),res)) time.sleep(random.randint(2,3)) if __name__ == '__main__': q=Queue() p=Process(target=procducer,args=(q,)) c=Process(target=consumer,args=(q,)) p.start() c.start() p.join() q.put(None) print('主')
注意:以上发送可以放在生产函数中循环完进行发送,当然也可以如上放在主进程中进行发送,但是前提是必须等生产子进程结束才可以。
========================用个小栗子来理解=================================================================
举一个小栗子,(寄信) 1、你把信写好——相当于生产者制造数据 2、你把信放入邮筒——相当于生产者把数据放入缓冲区 3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区 4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据 优势 缓冲区作用: 1、解耦 假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。
将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。 再举个小栗子 如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?
其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强 耦合)。
万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。
而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱 耦合)。 2:支持并发 生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只能一直等着 而使用这个模型,生产者把制造出来的数据只需要放在缓冲区即可,不需要等待消费者来取 再举个小栗子 从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);
又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,都挺耗时间的 3:支持忙闲不均 缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。
当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。 再举个小栗子 假设邮递员一次只能带走1000封信。万一某次碰上情人节送贺卡,需要寄出去的信超过1000封,
这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
二、生产者消费模型
总结:
---生产者消费者模型程序中两种角色:①负责生产数据(生产者);②负责处理数据(消费者)
---生产者消费者模型的作用:平衡生产者与消费者之间的速度差。
---实现方式:生产者——>队列——>消费者
如上篇博客内容关于生产消费模型内容,在生产者生产数据的过程结束后,即使消费者已将数据完全获取,消费者程序也不能结束,需由主进程或者生产者在结束生产程序后发送给消费者结束口令,消费者程序才会结束。但是如果出现多个消费者和多个生产者,这种情况又该如何解决?方法如下两种:
1、根据消费者数量传送结束信号(low)
from multiprocessing import Process,Queue import time,random,os def procducer(q): for i in range(10): res='包子%s' %i time.sleep(0.5) q.put(res) print('%s 生产了 %s' %(os.getpid(),res)) def consumer(q): while True: res=q.get() if res is None: break print('%s 吃 %s' %(os.getpid(),res)) time.sleep(random.randint(2,3)) if __name__ == '__main__': q=Queue() p=Process(target=procducer,args=(q,)) c=Process(target=consumer,args=(q,)) p.start() c.start() p.join() q.put(None) print('主')
from multiprocessing import Process,Queue import time import random import os def producer(name,q): for i in range(10): res='%s%s' %(name,i) time.sleep(random.randint(1, 3)) q.put(res) print('%s生产了%s' %(os.getpid(),res)) def consumer(name,q): while True: res=q.get() if not res:break print('%s吃了%s' %(name,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=('alex',q)) c2=Process(target=consumer,args=('egon',q)) _p=[p1,p2,p3,c1,c2] for p in _p: p.start() p1.join() p2.join() p3.join() '''保证生产程序结束后,再发送结束信号,发送数量和消费者数量一致''' q.put(None) q.put(None)
2、JoinableQueue队列机制
JoinableQueue与Queue队列基本相似,但前者队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。Queue实例的对象具有的方法JoinableQueue同样具有,除此JoinableQueue还具有如下方法:
①q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
②q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
from multiprocessing import Process,JoinableQueue import time import random def producer(name,food,q): for i in range(10): res='%s%s' %(food,i) time.sleep(random.randint(1, 3)) q.put(res) print('%s生产了%s' %(name,res)) q.join() #阻塞生产者进程,保证此进程结束时消费者进程已处理完其产生的数据 def consumer(name,q): while True: res=q.get() if not res:break print('%s吃了%s' %(name,res)) q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了 if __name__=='__main__': q=JoinableQueue() p1=Process(target=producer,args=(1,'巧克力',q)) p2=Process(target=producer,args=(2,'奶油蛋糕',q)) p3 = Process(target=producer, args=(3,'冰糖葫芦', q)) c1=Process(target=consumer,args=('lishi',q)) c2=Process(target=consumer,args=('jassin',q)) '''守护进程保证主进程结束时,守护进程也立即结束''' c1.daemon=True c2.daemon=True _p=[p1,p2,p3,c1,c2] for p in _p: p.start() p1.join() p2.join() p3.join()
二、回调函数
进程池执行完一个获得数据的进程,即刻要求通知主进程拿去解析数据。主进程调用一个函数去处理,这个函数便被称为回调函数,要求进程池进程的结果为回调函数的参数。
爬虫实例:
线程池
import requests from concurrent.futures import ThreadPoolExecutor(线程池),ProcessPoolExecutor(进程池) from threading import current_thread import time import os def get(url): # 下载 print('%s GET %s' %(current_thread().getName(),url)) response=requests.get(url) time.sleep(3) if response.status_code == 200: # 固定,=200表示下载完成 return {'url':url,'text':response.text} def parse(obj): # 解析 res=obj.result() print('[%s] <%s> (%s)' % (current_thread().getName(), res['url'],len(res['text']))) if __name__ == '__main__': urls = [ 'https://www.python.org', 'https://www.baidu.com', 'https://www.jd.com', 'https://www.tmall.com', ] t=ThreadPoolExecutor(2) for url in urls: t.submit(get,url).add_done_callback(parse) t.shutdown(wait=True) print('主',os.getpid())
我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。如果在主进程中等待进程池中所有任务都执行完毕后,再统一处理结果,则无需回调函数。
进程池
import requests from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import time import os def get(url): print('%s GET %s' %(os.getpid(),url)) response=requests.get(url) time.sleep(3) if response.status_code == 200: return {'url':url,'text':response.text} def parse(obj): res=obj.result() print('[%s] <%s> (%s)' % (os.getpid(), res['url'],len(res['text']))) if __name__ == '__main__': urls = [ 'https://www.python.org', 'https://www.baidu.com', 'https://www.jd.com', 'https://www.tmall.com', ] t=ProcessPoolExecutor(2) for url in urls: t.submit(get,url).add_done_callback(parse) t.shutdown(wait=True) print('主',os.getpid())