携程在手天下我有。。。。。哈哈开句玩笑,协程在python中可以说非常好用的一个功能
在讲之前我们先回顾一下 并发的定义,
并发:就是在一个时间段内通过不断的切换给人一种同时在完成多个事情的感觉,叫并发。。。其实本质上说他们是不同时的,不过然就是这么懒看不出来,又不同时那么叫不了并行就叫做并发吧。
定义
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
yield与协成
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
import time def consumer(): r='' while True: n= yield r if not n: return print('我要开始用啦%s' %n) time.sleep(1) r = '200 ok' def produce(c): next(c) n=0 while n<5: n=n+1 print('我生产完%s 啦 要给你哦' %n) cr = c.send(n) print('你用完了啊那我做下一个%s ' %cr) c.close() if __name__=='__main__': c=consumer() produce(c)
greenlet与协程
greenlet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务。greentlet是python中实现我们所谓的"Coroutine(协程)"的一个基础库.
from greenlet import greenlet def test1(): print (12) gr2.switch() print (34) gr2.switch() def test2(): print (56) gr1.switch() print (78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch()
重点gevent与协程
Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
import gevent import time def foo(): print('running in foo') gevent.sleep(2)#这里碰到了io的调用会切换到bar继续进行 print('switch to foo again') def bar(): print('switch to bar') gevent.sleep(3)#这里碰到了io调用会切换到foo上继续进行 print('switch to bar again') s=time.time() gevent.joinall(#这是一个有序的协程列表 [gevent.spawn(foo).#foo会第一个运行 gevent.spawn(bar)] #bar会第二个运行 ) print(time.time()-s)
当然这个方式没有更明显的对比效果我们做一个爬虫的实验
这里需要在
pip3 install gevent 和 pip3 install requests
一个是gevent协程模块
一个是网页获取模块
import gevent import monkey monkey.patch_all()#这里需要注意monkey实在gevent内部的一个方法我们在下面会调用gevent所以这里直接加载比较好 import gevent,requests,time def foo(url): res=requests.get(url).text print('get data %s' %len(res)) s=time.time() ''' foo('https://itk.org/') foo('https://www.github.com/') foo('https://zhihu.com/')#14.237622022628784 ''' gevent.joinall([gevent.spawn(foo,'https://itk.org/'), gevent.spawn(foo,'https://www.github.com/') , gevent.spawn(foo,'https://zhihu.com/') ] ) print(time.time()-s)#10.967933177947998
拓展
eventlet 是基于 greenlet 实现的面向网络应用的并发处理框架,提供“线程”池、队列等与其他 Python 线程、进程模型非常相似的 api,并且提供了对 Python 发行版自带库及其他模块的超轻量并发适应性调整方法,比直接使用 greenlet 要方便得多。
其基本原理是调整 Python 的 socket 调用,当发生阻塞时则切换到其他 greenlet 执行,这样来保证资源的有效利用。需要注意的是:
eventlet 提供的函数只能对 Python 代码中的 socket 调用进行处理,而不能对模块的 C 语言部分的 socket 调用进行修改。对后者这类模块,仍然需要把调用模块的代码封装在 Python 标准线程调用中,之后利用 eventlet 提供的适配器实现 eventlet 与标准线程之间的协作。
虽然 eventlet 把 api 封装成了非常类似标准线程库的形式,但两者的实际并发执行流程仍然有明显区别。在没有出现 I/O 阻塞时,除非显式声明,否则当前正在执行的 eventlet 永远不会把 cpu 交给其他的 eventlet,而标准线程则是无论是否出现阻塞,总是由所有线程一起争夺运行资源。所有 eventlet 对 I/O 阻塞无关的大运算量耗时操作基本没有什么帮助。
总结
协程的好处:
无需线程上下文切换的开销
无需原子操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序