本文对python协程做一些总结,来自平时的学习和一些博客资料
1. 异步IO
关于异步IO的讲解,网上资料太多了,我以前的文章也提到过,此处只做一些简单介绍:
- 异步IO采用消息循环的模式,重复“读取消息—处理消息”的过程
- 消息模型解决等待IO操作的问题:
- 程序发出IO请求,直接结束本轮消息处理,进入下一轮消息的处理
- 当IO操作完成守,将收到一条IO完成的消息,处理该消息时获取IO操作的结果
- 在IO操作的这段时间里,异步模型可以循环处理其他操作,而且没有线程切换的消耗,同时处理多个IO请求,适用于大多数IO密集型的应用程序。
python 的异步协程 (async coroutine) ,在提交 HTTP 请求后,就没必要等待请求完成再进一步操作,而是可以一边等着请求完成,一边做着其他工作。这可能在逻辑上需要多些思考来保证程序正确运行,但是好处是可以利用更少的资源做更多的事。
2. 协程概念
协程是python异步IO中常用的一个概率,英文为coroutine。协程不同于进程和线程,在某种程度上,它的概念更接近于函数调用的概念。
2.1 函数调用:
我们知道,程序执行中,函数调用是通过函数栈实现,因为栈FILO的特点,通常函数调用过程都是:A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。子程序(函数)调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
2.1 协程调用:
协程和函数最相似的地方就是协程本身的定义就更函数很像,而其调用也很相像。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
协程针对的是一个线程中的函数调用之间,所以没有线程切换,是在一个线程中轮流执行和终端多个函数而已,所以效率较高,而且不需要锁机制(只有一个线程执行)
需要注意的是,子程序内部中断的不是函数调用,而是被调用函数中断(一般来说可能是一条执行命令需要很长时间等待结果返回,比如常见的IO操作),转而去执行另一个函数(不是调用另一个函数),类似两个函数轮流执行,没有发生函数调用。
上述的执行流程看上去很像多线程有木有!!!实际上,一些协程的执行结果也很像多线程的执行结果,因为本质就是不同代码段轮流执行,进而提高效率。协程得到运用主要在于其效率更高:
协程的执行效率较高,因为不是线程切换,而是程序控制,没有开销。因为是一个线程中有多个协程,所以不需要锁机制。
此外,多线程编程中锁机制的开销在协程中也不存在,因为协程不需要锁机制:
因为协程执行是在一个线程中,不存在同时写变量冲突,所以共享资源不需要加锁,只需要判断状态就好,进一步提升效率。
3. python中的协程操作
python中协程的支持是通过生成器(generator)实现的。
熟悉generator的知道:函数定义时,将函数的结果通过yield返回,则定义了一个生成器,而在生成器中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。
而对于协程来说,yield不但可以返回一个值,它还能接收函数调用者发出的参数。(这句话很重要)
下面运用网上教程讲解的一个例子(我觉的很清晰)来说明协程操作:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
代码如下:
def consumer():
r = ''
while True:
n = yield r # 返回
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
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()
c = consumer()
produce(c)
要看懂上述代码,最重要的就是两个关键字的执行:send 和yield
- send:
熟悉python生成器的知道,调用生成器的next(),将运行到yield位置,此时暂停执行环境,并返回这条语句yield关键词后面跟随的值。 这是next()的使用方法。
而send本质和next相似,都是执行generator直到yield,不同的是send可以发送数据给yield表达式。
更重要的是,send发送的参数成为yield表达式的值!(通过这种方式赋值)
- yield:
普通函数return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。
而”yield”的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。
在python中,拥有这种能力的“函数”被称为生成器,只要只要函数中包含yield关键字,该函数调用就是生成器对象。生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。一个def主体中包含yield,则这个def主体自动变为generator
函数中每次使用yield产生一个值,函数就返回该值,然后停止执行,等待被激活,被激活后继续在原来的位置执行。
3.1 代码执行分析
熟悉了这两个关键字,来分析上述代码:
两个函数的定义不多说,生成一个consumer函数对象c,然后从produce(c)开始,注意第一条语句:
c.send(None)
这是启动一个生成器的操作,再次强调,send和next操作都是调用生成器,而第一调用生成器就是启动生成器,启动生成器必须使用next()语句或是send(None)启动生成器,不能使用send发送一个非None的值
此处需要强调:
这个操作其实已经执行了produce和consumer中的一部分,具体如下:
send类似next,调用了consumer,然后一直执行到yield语句,因为send调用发送的函数参数是None,None作为
n = yield r
中yield表达式的结果,将None赋值给了n,因为n为None,所以if成立,return返回;- 需要注意的是,此时consumer是return的,相当于本次生成器调用结束,而不是yield。
c.send(None)
表达式的结果为n = yield r
中yield后面的r,r初始值为''
,所以c.send(None)
结果为空,但是这个结果未赋值给任何变量
程序继续执行到:
n = n + 1
print()
r = c.send(n)
此时n = 1, 然后打印一行信息(生产了1),关键就是send语句。这条语句执行会有以下结果:
- c.send调用了生成器consumer的对象c,得到的是c中yield后面的值(yield返回结果)
- send发送了一个值n(=1),作为调用生成器的参数,并调用执行consumer();
- consumer生成器的对象c 获得这个调用参数,执行consumer(),一直到yield语句;
- consumer中yield表达式的结果就是send语句发送的参数n,然后将这个结果赋值给consumer中的n;
- 然后print打印Consuming信息,r赋值为‘200 0K’,循环到yield停止,返回r,这个返回值作为produce中c.send(n)调用结果,赋值给produce中的r,然后打印。
如此循环5次,直到c.close()关闭生成器consumer,结束。整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
Donald Knuth的一句话总结协程的特点:“子程序就是协程的一种特例。”
3.2 python3.5中的async和await
上面只是协程的简单操作,python中对协程的编程模型引入了一些标准库。
在 python3.5 中,创建一个协程仅仅只需使用 async 关键字,而python3.4使用 @asyncio.coroutine 装饰器。下面的任一代码,都可以作为协程工作,形式上也是等同的:
import asyncio
async def ping_server(ip): # 3.5
pass
@asyncio.coroutine
def load_file(path): # 3.4
pass
这俩特殊的函数,在调用时会返回协程对象。
3.1 中协程操作只是简单的生成器调用,常见的我们还需要在生成器或者说协程之间相互调用,用到yield from。yield from 用于一个generator调用另一个generator,主要是为了generator之间的调用。
yield from 表达式的使用方式如下:
import asyncio
@asyncio.coroutine
def get_jason(client, url):
file_content = yield from load_file('/Usrs/scott/data.txt')
正如所看到的, yield from 被使用在用 @asyncio.coroutine 装饰的函数内(将这个函数装饰成一个生成器),如果想把 yield from 在这个函数外使用,将会抛出如下语法错误:
File "main.py", line 1
file_content = yield from load_file('/Users/scott/data.txt')
^
SyntaxError: 'yield' outside function
较新的语法是使用 async/await 关键字。 async 从 Python3.5 开始被引进,跟 @asyncio.coroutine 装饰器一样,用来声明一个函数是一个协程。只要把它放在函数定义之前,就可以应用到函数上,使用方式如下:
async def ping_server(ip):
# ping code here...
实际调用这个函数时,使用 await 而不用 yield from,当然,使用方式依然差不多:
async def ping_local(ip):
return await ping_server('192.168.1.1')
再强调一遍,跟 yield from 一样,不能在函数外部使用 await,否则会抛出语法错误。 (async 用来声明一个函数是协程,然后使用 await调用这个协程, await 必须在函数内部,这个函数通常也被声明为另一个协程)
Python3.5 对这两种调用协程的方法都提供了支持,但是推荐 async/await 作为首选。
4. Event Loop
asyncio是python异步IO的标准支持,其编程模型就是一个消息循环,其中事件循环模块就是核心。
如果不知道如何开始和操作一个 Eventloop,那么上诉有关协程所说的都起不了多大作用。Eventloop 在执行异步函数时非常重要,重要到只要执行协程,基本上就得利用 Eventloop 。
Eventloop 提供了相当多的功能:
- 注册,执行和取消延迟调用(异步函数)
- 创建客户端与服务端传输用于通信
- 创建子程序和通道跟其他的程序进行通信
- 指定函数的调用到线程池
Eventloop 有相当多的配置和类型可供使用,但大部分程序只需要如下方式预定函数即可:
import asyncio
async def hello():
print("Hello World")
r = await asyncio.sleep(1)
print("Again")
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
loop.close()
分析上述代码:async把一个函数标记为coroutine类型,这个函数调用会返回协程对象。然后,我们就把这个coroutine扔到EventLoop中执行。
hello()会首先打印出Hello world,然后,await语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
加粗部分不好理解,如果我们往EventLoop中添加多个任务,就能看出效果。
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
结果:
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
(暂停约1秒)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)
由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。
(上述用例引自:摸我)
需要注意,loop.run_until_complete() 函数实际上是阻塞性的,也就是在所有异步方法完成之前,它是不会返回的。