python 如何引入协程和原理分析
相关概念
- 并发:指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个程序在cpu上运行。比如说在一秒内cpu切换了100个进程,就可以认为cpu的并发是100。
- 并行:值任意时刻点上,有多个程序同时运行在cpu上,可以理解为多个cpu,每个cpu独立运行自己程序,互不干扰。并行数量和cpu数量是一致的。
我们平时常说的高并发而不是高并行,是因为cpu的数量是有限的,不可以增加。
形象的理解:cpu对应一个人,程序对应喝茶,人要喝茶需要四个步骤(可以对应程序需要开启四个线程):1烧水,2备茶叶,3洗茶杯,4泡茶。
并发方式:烧水的同时做好2备茶叶,3洗茶杯,等水烧好之后执行4泡茶。这样比顺序执行1234要省时间。
并行方式:叫来四个人(开启四个进程),分别执行任务1234,整个程序执行时间取决于耗时最多的步骤。
- 同步 (注意同步和异步只是针对于I/O操作来讲的)值调用IO操作时,必须等待IO操作完成后才开始新的的调用方式。
- 异步指调用IO操作时,不必等待IO操作完成就开始新的的调用方式。
- 阻塞 指调用函数的时候,当前线程被挂起。
- 非阻塞 指调用函数的时候,当前线程不会被挂起,而是立即返回。
IO多路复用
sllect,poll,epoll都是IO多路复用的机制。IO多路复用就是通过这样一种机制:一个进程可以监听多个描述符,一旦某个描述符就绪(一般是读就绪和写就绪),能够通知程序进行相应的操作。但select,poll,epoll本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写(即将数据从内核空间拷贝到应用缓存)。也就是说这个读写过程是阻塞的。而异步IO则无需自己负责读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
select
select函数监听的文件描述符分三类:writefds、readfds、和exceptfds。调用后select函数会阻塞,直到描述符就绪(有数据可读、写、或者有except)或者超时(timeout指定等待时间,如果立即返回则设置为null),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
优点:良好的跨平台性(几乎所有的平台都支持)
缺点:单个进程能够监听的文件描述符数量存在最大限制,在linux上一般为1024,可以通过修改宏定义甚至重新编译内核来提升,但是这样也会造成效率降低。
poll
不同于select使用三个位图来表示fdset的方式,poll使用的是pollfd的指针实现
pollfd结构包含了要监听的event和发生的event,不再使用select“参数-值”传递的方式。同时pollfd并没有最大数量限制(但是数量过大之后性能也是会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会下降。
epoll
epoll是在linux2.6内核中国提出的,(windows不支持),是之前的select和poll增强版。相对于select和poll来说,epoll更加灵活,没有描述符的限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的时间存放到内核的一个时间表中。这样在用户控件和内核控件的coppy只需要一次。
如何选择?
①在并发高同时连接活跃度不是很高的请看下,epoll比select好(网站或web系统中,用户请求一个页面后随时可能会关闭)
②并发性不高,同时连接很活跃,select比epoll好。(比如说游戏中数据一但连接了就会一直活跃,不会中断)
省略章节:由于在用到select的时候需要嵌套多层回调函数,然后印发一系列的问题,如可读性差,共享状态管理困难,出现异常排查复杂,于是引入协程,既操作简单,速度又快。
协程
对于上面的问题,我们希望去解决这样几个问题:
- 采用同步的方式去编写异步的代码,使代码的可读性高,更简便。
- 使用单线程去切换任务(就像单线程间函数之间的切换那样,速度超快)
(1)线程是由操作系统切换的,单线程的切换意味着我们需要程序员自己去调度任务。
(2)不需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高。
例如我们在做爬虫的时候:
defget_url(url): html=get_html(url)#此处网络下载IO操作比较耗时,希望切换到另一个函数去执行 infos=parse_html(html) #下载url中的html defget_html(url): pass #解析网页 defparse_html(html): pass
意味着我们需要一个可以暂停的函数,对于此函数可以向暂停的地方穿入值。(回忆我们的生成器函数就可以满足这两个条件)所以就引入了协程。
生成器进阶
- 生成器不仅可以产出值,还可以接收值,用send()方法。注意:在调用send()发送非None值之前必须先启动生成器,可以用①next()②send(None)两种方式激活
defgen_func(): html=yield'http://www.baidu.com'#yield前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值 print(html) yield2 yield3 return'bobby' if__name__=='__main__': gen=gen_func() url=next(gen) print(url) html='bobby' gen.send(html)#send方法既可以将值传递进生成器内部,又可以重新启动生成器执行到下一yield位置。 打印结果: http://www.baidu.com bobby
- close()方法。
defgen_func(): yield'http://www.baidu.com'#yield前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值 yield2 yield3 return'bobby' if__name__=='__main__': gen=gen_func() url=next(gen) gen.close() next(gen) 输出结果: StopIteration
特别注意:调用close.()之后,生成器在往下运行的时候就会产生出一个GeneratorExit,单数如果用try捕获异常的话,就算捕获了遇到后面还有yield的话,还是不能往下运行了,因为一旦调用close方法生成器就终止运行了(如果还有next,就会会产生一个异常)所以我们不要去try捕捉该异常。(此注意可以先忽略)
defgen_func(): try: yield'http://www.baidu.com' exceptGeneratorExit: pass yield2 yield3 return'bobby' if__name__=='__main__': gen=gen_func() print(next(gen)) gen.close() next(gen) 输出结果: RuntimeError:generatorignoredGeneratorExit
- 调用throw()方法。用于抛出一个异常。该异常可以捕捉忽略。
defgen_func(): yield'http://www.baidu.com'#yield前面加=号就实现了1:可以产出值2:可以接受调用者传过来的值 yield2 yield3 return'bobby' if__name__=='__main__': gen=gen_func() print(next(gen)) gen.throw(Exception,'DownloadError') 输出结果: DownloadError
yieldfrom
先看一个函数:fromitertoolsimportchain
fromitertoolsimportchain my_list=[1,2,3] my_dict={'frank':'yangchao','ailsa':'liuliu'} forvalueinchain(my_list,my_dict,range(5,10)):chain()方法可以传入多个可迭代对象,然后分别遍历之。 print(value) 打印结果: 1 2 3 frank ailsa 5 6 7 8 9
此函数可以用yieldfrom实现:yieldfrom功能1:从一个可迭代对象中将值逐个返回。
my_list=[1,2,3] my_dict={'frank':'yangchao','ailsa':'liuliu'} defchain(*args,**kwargs): foritemrableinargs: yieldfromitemrable forvalueinchain(my_list,my_dict,range(5,10)): print(value)
看如下代码:
defgen(): yield1 defg1(gen): yieldfromgen defmain(): g=g1(gen) g.send(None)
代码分析:此代码中main调用了g1,main就叫作调用方,g1叫做委托方,gen叫做子生成器yieldfrom将会在调用方main与子生成器gen之间建立一个双向通道。(意味着可以直接越过委托方)
例子:当委托方middle()中使用yieldfrom的时候,调用方main直接和子生成器sales_sum形成数据通道。
final_result={} defsales_sum(pro_name): total=0 nums=[] whileTrue: x=yield print(pro_name+'销量',x) ifnotx: break total+=x nums.append(x) returntotal,nums#程序运行到return的时候,会将return的返回值返回给委托方,即middle中的final_result[key] defmiddle(key): whileTrue:#相当于不停监听sales_sum是否有返回数据,(本例中有三次返回) final_result[key]=yieldfromsales_sum(key) print(key+'销量统计完成!!') defmain(): data_sets={ '面膜':[1200,1500,3000], '手机':[88,100,98,108], '衣服':[280,560,778,70], } forkey,data_setindata_sets.items(): print('startkey',key) m=middle(key) m.send(None)#预激生成器 forvalueindata_set: m.send(value) m.send(None)#发送一个None使sales_sum中的x值为None退出while循环 print(final_result) if__name__=='__main__': main() 结果: startkey面膜 面膜销量1200 面膜销量1500 面膜销量3000 面膜销量None 面膜销量统计完成!! startkey手机 手机销量88 手机销量100 手机销量98 手机销量108 手机销量None 手机销量统计完成!! startkey衣服 衣服销量280 衣服销量560 衣服销量778 衣服销量70 衣服销量None 衣服销量统计完成!! {'面膜':(5700,[1200,1500,3000]),'手机':(394,[88,100,98,108]),'衣服':(1688,[280,560,778,70])}
也许有人会好奇,为什么不能直接用main()函数直接去调用sales_sum呢?加一个委托方使代码复杂化了。看以下直接用main()函数直接去调用sales_sum代码:
defsales_sum(pro_name): total=0 nums=[] whileTrue: x=yield print(pro_name+'销量',x) ifnotx: break total+=1 nums.append(x) returntotal,nums if__name__=='__main__': my_gen=sales_sum('面膜') my_gen.send(None) my_gen.send(1200) my_gen.send(1500) my_gen.send(3000) my_gen.send(None) 输出结果: 面膜销量1200 面膜销量1500 面膜销量3000 面膜销量None Traceback(mostrecentcalllast): File"D:/MyCode/Cuiqingcai/Flask/test01.py",line56,inmy_gen.send(None) StopIteration:(3,[1200,1500,3000])
从上述代码可以看出,即使数据return结果出来了,还是会返回一个exception,由此可以看出yieldfrom的一个最大优点就是当子生成器运行时候出现异常,yieldfrom可以直接自动处理这些异常。
yieldfrom功能总结:
子生成器生产的值,都是直接给调用方;调用发通过.send()发送的值都是直接传递给子生成器,如果传递None,会调用子生成器的next()方法,如果不是None,会调用子生成器的sen()方法。
子生成器退出的时候,最后的returnEXPR,会触发一个StopIteration(EXPR)异常
yieldfrom表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数。
如果调用的时候出现了StopIteration异常,委托方生成器恢复运行,同时其他的异常向上冒泡。
传入委托生成器的异常里,除了GeneratorExit之后,其他所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上冒泡
如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有就不调用,如果在调用.close()时候抛出了异常,那么就向上冒泡,否则的话委托生成器跑出GeneratorExit异常。
以上就是python如何引入协程和原理分析的详细内容,更多关于python协程的资料请关注毛票票其它相关文章!