Python与协程从Python2—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 < 5:

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操作异步化。

原文发布于微信公众号 - 嘉为科技(canway_service)

原文发表时间:2019-06-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券