协程,又称微执行绪、纤程,英文名Coroutine;用一句话说明什么是执行绪的话:协程是一种使用者态的轻量级执行绪。
Python对于协程的支援在python2中还比较简单,但是也有可以使用的第三方库,在python3中开始全面支援,也成为python3的一个核心功能,很值得学习。
协程介绍
协程,又称微执行绪、纤程,英文名Coroutine;用一句话说明什么是执行绪的话:协程是一种使用者态的轻量级执行绪。
协程拥有自己的暂存器上下文和栈。协程排程切换时,将暂存器上下文和栈储存到其他地方,在切回来的时候,恢复先前储存的暂存器上下文和栈。因此:协程能保留上一次呼叫时的状态(即所有区域性状态的一个特定组合),每次过程重入时,就相当于进入上一次呼叫的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的优点:
1)无需执行绪上下文切换的开销
2)无需原子操作锁定及同步的开销
3)方便切换控制流,简化程式设计模型
4)高并发+高扩充套件性+低成本:一个CPU支援上万的协程都不是问题。所以很适合用于高并发处理。
协程的缺点:
1)无法利用多核资源:协程的本质是个单执行绪,它不能同时将 单个CPU 的多个核用上,协程需要和程序配合才能执行在多CPU上
2)进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程式
Python2中的协程
yield关键字
Python2对于协程的支援,是通过yield关键字实现的,下面示例程式码是一个常见的生产者—消费者模型,程式码示例如下:
def consumer:
r = \\
while True:
n = yield r
if not n:
continue
print(\[CONSUMER] Consuming %s...\ % n)
r = \200 OK\
def produce(c):
c.next
n = 0
while n
n = n + 1
print(\[PRODUCER] Producing %s...\ % n)
r = c.send(n)
print(\[PRODUCER] Consumer return: %s\ % r)
c.close
if __name__ == \__main__\:
c = consumer
produce(c)
执行结果:
注意到consumer函式是一个generator(生成器),把一个consumer传入produce后:
1)首先呼叫c.next启动生成器;
2)然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
3)consumer通过yield拿到讯息,处理,又通过yield把结果传回;
4)produce拿到consumer处理的结果,继续生产下一条讯息;
5)produce决定不生产了,通过c.close关闭consumer,整个过程结束。
整个流程无锁,由一个执行绪执行,produce和consumer协作完成任务,所以称为“协程”,而非执行绪的抢占式多工。
传统的生产者-消费者模型是一个执行绪写讯息,一个执行绪取讯息,通过锁机制控制伫列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产讯息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
Python对协程的支援还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支援不完全,但已经可以发挥相当大的威力了。
gevent模组
Python通过yield提供了对协程的基本支援,但是不完全。而第三方的gevent为Python提供了比较完善的协程支援。gevent是第三方库,通过greenlet实现协程,其基本思想是:当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程式处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在执行,而不是等待IO。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。
示例程式码如下:
from gevent import monkey; monkey.patch_all
import gevent
import urllib2
def f(url):
print(\GET: %s\ % url)
resp = urllib2.urlopen(url)
data = resp.read
print(\%d bytes received from %s.\ % (len(data), url))
gevent.joinall([
gevent.spawn(f, \https://www.python.org/\),
gevent.spawn(f, \https://www.yahoo.com/\),
gevent.spawn(f, \https://github.com/\),
])
执行结果:
从执行结果可以看到,网站访问的顺序是自动切换的。
gevent优缺
使用gevent,可以获得极高的并发效能,但gevent只能在Unix/Linux下执行,在Windows下不保证正常安装和执行。Python创始人Gvanrossum从来不喜欢Gevent,而是更愿意另辟蹊径的实现asyncio(python3中的异步实现)。1)Monkey-patching。中文“猴子补丁”,常用于对测试环境做一些hack。Gvanrossum说用它就是”patch-and-pray”,由于Gevent直接修改标准库里面大部分的阻塞式系统呼叫,包括socket、ssl、threading和 select等模组,而变为协作式执行。但是无法保证在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题,那么只能祈祷(pray)了。
2)其次,在Python之禅中明确说过:“Explicit is better than implicit.”,猴子补丁明显的背离了这个原则。
3)第三方库支援。得确保专案中用到其他用到的网络库也必须使用纯Python或者明确说明支援Gevent,而且就算有这样的第三方库,也需要担心这个第三方库的程式码质量和功能性。
4)Greenlet不支援Jython和IronPython,这样就无法把gevent设计成一个标准库了。
之前是没有选择,很多人选择了Gevent,而现在明确的有了更正统的、正确的选择:asyncio(下一节会介绍)。所以建议大家了解Gevent,拥抱asyncio。
另外,如果知道现在以及未来使用Gevent不会给专案造成困扰,那么用Gevent也是可以的。
Python3中的协程
Gvanrossum希望在Python 3 实现一个原生的基于生成器的协程库,其中直接内建了对异步IO的支援,这就是asyncio,它在Python 3.4被引入到标准库。
下面将简单介绍asyncio的使用:
1)event_loop 事件循环:程式开启一个无限的循环,程序员会把一些函式注册到事件循环上。当满足事件发生的时候,呼叫相应的协程函式。
2)coroutine 协程:协程物件,指一个使用async关键字定义的函式,它的呼叫不会立即执行函式,而是会返回一个协程物件。协程物件需要注册到事件循环,由事件循环呼叫。
3)task 任务:一个协程物件就是一个原生可以挂起的函式,任务则是对协程进一步封装,其中包含任务的各种状态。
4)future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
5)async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步呼叫界面。
程式码示例如下:
import asyncio
import time
now = lambda: time.time
async def do_some_work(x):
print(\Waiting: {}s\.format(x))
await asyncio.sleep(x)
return \Done after {}s\.format(x)
async def main:
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(5)
coroutine3 = do_some_work(3)
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
done, pending = await asyncio.wait(tasks)
for task in done:
print(\Task ret: \, task.result)
start = now
loop = asyncio.get_event_loop
task = asyncio.ensure_future(main)
try:
loop.run_until_complete(task)
print(\TIME: \, now - start)
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks)
print(asyncio.gather(*asyncio.Task.all_tasks).cancel)
loop.stop
loop.run_forever
finally:
loop.close
执行结果:
可以看到程式执行时间是以等待时间最长的为准。
使用async可以定义协程物件,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函式让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。耗时的操作一般是一些IO操作,例如网络请求,档案读取等。我们使用asyncio.sleep函式来模拟IO操作。协程的目的也是让这些IO操作异步化。
Asyncio是python3中一个强大的内建库,上述只是简单的介绍了asyncio的用法有兴趣的话,很值得去学习一下!