Day13- Python基础13 生产者与消费者模型、进程、线程、协程
本节内容:
1:生产者与消费者
2:进程调用两种
3:进程Process的方法
4:进程间的通信1 queue 队列
5:进程间的通信2 Pipe 管道
6:进程间的数据共享 Managers
7:进程同步
8:进程池
9:协程
1.生产者与消费者
生产者消费者模型:
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这就像,在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可,这也是一个结耦的过程。
import queue import threading import time q = queue.Queue() class Producer(threading.Thread): def __init__(self,name): threading.Thread.__init__(self) self.name = name def run(self): for i in range(1,10): print('making....') print("priducer %s make %s 包子."%(self.name,i)) q.put(i) ## 放进队列之后 q.task_done() ##发送一个信号,通知q里面有数据了 time.sleep(1) class Customer(threading.Thread): def __init__(self,name): threading.Thread.__init__(self) self.name = name def run(self): while 1: ##一直在吃 q.join() ##当有信号来了我就去包子笼去取, data = q.get() print("customer %s eat %s baozi"%(self.name,data)) p = Producer("小当家") c = Customer("B君") p.start() c.start()
2.进程的调用
进程的调用和线程的调用是一样的,有直接调用和类调用两种方式。
调用方式1:直接调用
from multiprocessing import Process import time def f(name): time.sleep(1) print('hello', name,time.ctime()) if __name__ == '__main__': p_list=[] for i in range(3): p = Process(target=f, args=('alvin',)) p_list.append(p) p.start() for i in p_list: p.join() print('end')
调用方式2:类调用
from multiprocessing import Process import time class MyProcess(Process): def __init__(self): super(MyProcess, self).__init__() #self.name = name def run(self): time.sleep(1) print ('hello', self.name,time.ctime()) if __name__ == '__main__': p_list=[] for i in range(3): p = MyProcess() p.start() p_list.append(p) for p in p_list: p.join() print('end')
3.进程Process的方法
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前还没有实现,库引用中提示必须是None;
target: 要执行的方法;
name: 进程名;
args/kwargs: 要传入方法的参数。
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样,不过就是进程的daemon的属性,使用方式:p.daemon = False
name:进程名字。
pid:进程号。
import time from multiprocessing import Process def foo(i): time.sleep(1) print (p.is_alive(),i,p.pid) time.sleep(1) if __name__ == '__main__': p_list=[] for i in range(10): p = Process(target=foo, args=(i,)) #p.daemon=True p_list.append(p) for p in p_list: p.start() # for p in p_list: # p.join() print('main process end')
4:进程间的通信1 queue 队列
要实现的代码:子进程放数据 ,主线程取数据
import queue import multiprocessing def foo(q): q.put(12) # print('id q:',id(q)) if __name__ == '__main__': # q = queue.Queue() ##这是一个线程队列,我要用进程队列 q = multiprocessing.Queue() print('id q:', id(q)) p = multiprocessing.Process(target=foo,args=(q,)) ##创建的q是在主进程中,要想使用需要给子进程传过去;子进程复制了q的 p.start() # p.join() data = q.get() ##队列为空,就阻塞住 print(data)
5:进程间的通信2 Pipe 管道
Pipe有点类似socket的conn ;他是一个双向管道,parent_conn, child_conn = Pipe() 这样拿到了2个双向管道 ;
Pipe 一端给父亲 ,一端给儿子 ,然后父亲和儿子就可以进行通信了 ;
Pipe的send 和recv 和socket的接口类似,但是不走网络。所以不需要转字节;
import multiprocessing def foo(conn): conn.send("{'name':'yhy','content':'爸爸你好'}") data = conn.recv() print(data) if __name__ == '__main__': parent_conn,child_conn = multiprocessing.Pipe() ##创建了管道 p = multiprocessing.Process(target=foo,args=((child_conn,))) ##一端给儿子 p.start() parent_conn.send('儿子在吗') data = parent_conn.recv() print(data)
6:进程间的数据共享 Managers
Queue和pipe只是实现了数据交互,并没实现数据共享,数据共享即一个进程去更改另一个进程的数据。
可以进程数据共享的数据结构有如下几种:但是要记住这些的数据结构都是要经过,manager封装过的数据类型。
types list
, dict
, Namespace
, Lock
, RLock
, Semaphore
, BoundedSemaphore
, Condition
, Event
, Barrier
, Queue
, Value
and Array
.
from multiprocessing import Process, Manager def f(d, l,n): d[n] = '1' d['2'] = 2 l.append(n) # print("son process:",id(d),id(l)) if __name__ == '__main__': with Manager() as manager: ##类似文件操作的with操作,我们可以不必手动的colose d = manager.dict() ##字典是经过manager封装过的字典 l = manager.list(range(5)) # print("main process:",id(d),id(l)) p_list = [] for i in range(10): p = Process(target=f, args=(d,l,i)) p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l) # 输出: # {0: '1', 1: '1', 2: '1', 3: '1', 4: '1', 5: '1', 6: '1', 7: '1', 8: '1', 9: '1', '2': 2} # [0, 1, 2, 3, 4, 0, 1, 2, 3, 6, 4, 7, 8, 5, 9]
7:进程同步
需知:
什么是同步?
有资源在阻塞,处于一个阻塞的状态。
为什么有同步锁?
控制一次只允许一个线程对同一个数据进行操作 。
进程的数据是相互独立的,那我进程还用同步干什么?
说是这么多,但是多个进程,也会面临共用同一个资源的时候,比如说屏幕。
代码实现内容:开启十个进程依次打印print('hello world %s' % i)
from multiprocessing import Process, Lock def f(l, i): # with l.acquire(): print('hello world %s' % i) if __name__ == '__main__': lock = Lock() for num in range(10): Process(target=f, args=(lock, num)).start()
输出:屏幕是共用的资源,进程0和进程1都想在屏幕打印信息,出现错乱 。
解决的办法:加锁,而锁是multiprocessing 下的Lock
from multiprocessing import Process, Lock def f(l, i): with l: ##with之后 自动acquire 自动release print('hello world %s' % i) if __name__ == '__main__': lock = Lock() for num in range(10): Process(target=f, args=(lock, num)).start()
8: 进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
- apply 阻塞同步
- apply_async 异步状态
进程池,就是定义一个池了,我只开固定的几个进程去反复的执行某个动作。
from multiprocessing import Process,Pool import time,os def Foo(i): time.sleep(3) print(i) return i+100 def Bar(arg): ##回调函数必须要有一个形参去,接收foo的返回值 print(arg) print('hello') if __name__ == '__main__': pool = Pool() ##这就是池,定义最大为5个,要是不定义 那么默认是按你cpu的核数去跑。 for i in range(100): #pool.apply(func=Foo, args=(i,)) ##提供了同步接口,串行 #pool.apply_async(func=Foo, args=(i,)) ##回调函数,callback 就是某个动作或者函数执行成功之后再去执行的函数,回调函数是主进程执行的 pool.apply_async(func=Foo, args=(i,),callback=Bar) pool.close() ##进程池的格式是死的,必须先close再join pool.join() print('end')
9:协程
协程,又称微线程,协程执行看起来有点像多线程,但是事实上协程就是只有一个线程,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显,此外因为只有一个线程,不需要多线程的锁机制,也不存在同时写变量冲突。协程的适用场景:当程序中存在大量不需要CPU的操作(IO)阻塞时。
默认线程和进程都是抢占式的,当我们开启多个的线程和进程时候,哪个会抢到cpu的执行,无法预料。 协程将cpu的执行流程交到了用户手上。 协程是也是多任务实现方式,它不需要多个进程或线程就可以实现多任务。
一定要理解协程相当于是任务,多个协程就相当于多个任务,然后程序会一直去执行这些任务的。
至于在执行任务的时候如何让出cpu控制权,这个就要用await 关键字,让当前的任务让出cpu给别人执行。
9.1:定义一个协程
定义一个协程很简单,使用async关键字,就像定义普通函数一样:
import asyncio # 使用关键字async 去定义个方法 async def func(): print("task") # 3.7版本之前的调用方式 loop = asyncio.get_event_loop() corotine = func() # print(type(corotine)) # <class 'coroutine'> 他是一个协程对象 ,这里会报错,说你没有把协程对象事件循环中 # asyncio.ensure_future(corotine) # 使用这个加入到事件任务中去,后面会讲 loop.run_until_complete(func()) # 3.7 可以直接只用run,而不用定义一个事件循环 # asyncio.run(func()) #task
9.2:显示的定义任务
在上面例子中,我们直接用 loop.run_until_complete(func())。代码将协程对象放到run_until_complete去执行。
这个实际上,他的内部会创建一个task任务组,然后将协程对象加到这个任务组,最后让程序去管理这些任务组的。
那么我们如何自己定义任务组?
asyncio.ensure_future
loop.create_task(coroutine)
import asyncio async def do_some_work2(x): print("waiting:",x) async def do_some_work(x): print("waiting:",x) # 我们在这里直接用ensure_future加一个协程任务,到task中去。 # 可以点击进去查看源码,可以不难看出,如果不存在task就会创建全局task并且将协程加入到全局的task中去 # 如果存在全局task就直接将这个协程对象,加到已经存在的task去 asyncio.ensure_future(do_some_work2(3)) loop = asyncio.get_event_loop() # 这里我们显示的创建出task,然后将协程对象加到task任务去 # 在上面的01案例,我们没有定义task ,但是run_until_complete(协程对象) 也是会跟ensure_future差不多,会先创建一个task然后去执行他 coroutine = do_some_work(2) task = loop.create_task(coroutine) loop.run_until_complete(task)
9.3:协程绑定回调
在9.2中,我们使用ensure_future,去创建一个任务的好处也是我们可以绑定回调函数
import asyncio async def func(): return "done","eqwe" def func_callback(x:asyncio.Task,*args): print("***",x) print(type(x)) # <class '_asyncio.Task'> print(x.result()) #('done', 'eqwe' print(*args) # "" loop = asyncio.get_event_loop() task = asyncio.ensure_future(func()) task.add_done_callback(func_callback) # 通过创建任务是可以绑定回调函数的 loop.run_until_complete(task)
9.4:阻塞和await
在协程最开始的时候,就说了我们创建协程是为了加快速度的。当遇见IO请求的时候,可以让出cpu控制权,让程序去执行其他的协程任务。
实现这个功能的关键就是用await关键字!
await 后面可以等待的对象是【协程对象、Future、Task对象 ->IO等待】
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。 协程遇到await,事件循环将会挂起该协程, 执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。 耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。
这里我们定义了两个函数。对比下传统执行方法的方式和使用协程的方式
import asyncio import time def fun1(): print("开始任务") time.sleep(2) return "任务结束" async def func2(): print("开始任务") await asyncio.sleep(2) return "任务结束" def func2_callbask(x): print(x.result(),'我是回调结果') if __name__ == '__main__': # start_time = time.time() # for i in range(2): # result = fun1() # print(result) #任务结束 # print("用时:",time.time()-start_time) # 用时: 4.010415077209473 # 没有绑定回调结果的 # start_time = time.time() # task = [] # for i in range(2): # task.append(func2()) # loop = asyncio.get_event_loop() # loop.run_until_complete(asyncio.wait(task)) # print("用时:",time.time()-start_time) # 用时: 2.0094027519226074 ,非常明显的差距 # 绑定回调结果的 start_time = time.time() task_list = [] for i in range(2): task = asyncio.ensure_future(func2()) task.add_done_callback(func2_callbask) task_list.append(task) loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(task_list)) print("用时:",time.time()-start_time) # 用时: 2.0094027519226074 ,非常明显的差距
9.5:协程嵌套
在9.2的时候,我们其实就实现了一个嵌套。
使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来。
import asyncio import time async def do_some_work(x): print('Waiting: ', x) await asyncio.sleep(x) return 'Done after {}s'.format(x) async def main(): coroutine1 = do_some_work(1) coroutine2 = do_some_work(2) coroutine3 = do_some_work(4) tasks = [ asyncio.ensure_future(coroutine1), asyncio.ensure_future(coroutine2), asyncio.ensure_future(coroutine3) ] # 这里的wait,他的传参是一个协程列表,内部会获取到一个正在运行的loop,然后将任务加到事件循环中 # dones, pendings = await asyncio.wait(tasks) # for task in dones: # print('Task ret: ', task.result()) # 如果使用的是 asyncio.gather创建协程对象,那么await的返回值就是协程运行的结果。 results = await asyncio.gather(*tasks) for result in results: print('Task ret gather: ', result) start = time.time() loop = asyncio.get_event_loop() loop.run_until_complete(main()) print('TIME: ', time.time() - start)
9.6:异步加多线程的方式
比如说同时下载多文件,碰到阻塞的函数则用run_in_executor来新开线程跳过阻塞。
import asyncio import os import requests async def downOne(loop, url): name = os.path.basename(url) print("downOne:start {}".format(name)) # loop = asyncio.get_event_loop() # requests.get(url) result = await loop.run_in_executor(None, requests.get, url) print("downOne:middle {}".format(name)) if result.status_code == 200: with open(name, "wb") as fp: # fp.write(result.content) r = await loop.run_in_executor(None, fp.write, result.content) print("downOne: r = {}".format(r)) print("downOne:endxx {}".format(name)) return "ok {}".format(name) async def deal(loop): ts = [] for i in range(1, 2): path = "http://www.baidu.com" ts.append(asyncio.ensure_future(downOne(loop, path))) print("deal: middle") result = await asyncio.gather(*ts) print("deal:end") return result if __name__ == "__main__": loop = asyncio.get_event_loop() try: result = loop.run_until_complete(deal(loop)) print("main:result = {}".format(result)) finally: loop.close()
案例2:异步加线程获取链接
import asyncio import requests def fetch(): response = requests.get("http://httpbin.org/delay/2") print(len(response.text)) async def run(loop): task_list = [] for i in range(6): task = asyncio.ensure_future(loop.run_in_executor(None,fetch)) task_list.append(task) a,b = await asyncio.wait(task_list) print(a) print(b) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(run(loop))
在linux中我们可以只用uvloop来替代我们原本的异步控制器: