看下Python中常见的几种函数形式:
1 普通函数
1 | def function(): |
2 生成器函数
1 | def generator(): |
在3.5过后,我们可以使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。
3 异步函数(协程)
1 | async def async_function(): |
4 异步生成器
1 | async def async_generator(): |
通过类型判断可以验证函数的类型
1 | import types |
直接调用异步函数不会返回结果,而是返回一个coroutine对象:
1 | print(async_function()) |
协程需要通过其他方式来驱动,因此可以使用这个协程对象的send方法给协程发送一个值:
1 | print(async_function().send(None)) |
不幸的是,如果通过上面的调用会抛出一个异常:
1 | StopIteration: 1 |
因为生成器/协程在正常返回退出时会抛出一个StopIteration异常,而原来的返回值会存放在StopIteration对象的value属性中,通过以下捕获可以获取协程真正的返回值:
1 | try: |
通过上面的方式来新建一个run函数来驱动协程函数:
1 | def run(coroutine): |
在协程函数中,可以通过await语法来挂起自身的协程,并等待另一个协程完成直到返回结果:
1 | async def async_function(): |
要注意的是,await语法只能出现在通过async修饰的函数中,否则会报SyntaxError错误。
而且await后面的对象需要是一个Awaitable,或者实现了相关的协议。
查看Awaitable抽象类的代码,表明了只要一个类实现了await方法,那么通过它构造出来的实例就是一个Awaitable:
1 | class Awaitable(metaclass=ABCMeta): |
而且可以看到,Coroutine类也继承了Awaitable,而且实现了send,throw和close方法。所以await一个调用异步函数返回的协程对象是合法的。
1 | class Coroutine(Awaitable): |
接下来是异步生成器,来看一个例子:
假如我要到一家超市去购买土豆,而超市货架上的土豆数量是有限的:
1 | class Potato: |
现在我想要买50个土豆,每次从货架上拿走一个土豆放到篮子:
1 | def take_potatos(num): |
对应到代码中,就是迭代一个生成器的模型,显然,当货架上的土豆不够的时候,这时只能够死等,而且在上面例子中等多长时间都不会有结果(因为一切都是同步的),也许可以用多进程和多线程解决,而在现实生活中,更应该像是这样的:
1 | async def take_potatos(num): |
当货架上的土豆没有了之后,我可以询问超市请求需要更多的土豆,这时候需要等待一段时间直到生产者完成生产的过程:
1 | async def ask_for_potato(): |
当生产者完成和返回之后,这是便能从await挂起的地方继续往下跑,完成消费的过程。而这整一个过程,就是一个异步生成器迭代的流程:
1 | async def buy_potatos(): |
async for语法表示我们要后面迭代的是一个异步生成器。
1 | def main(): |
用asyncio运行这段代码,结果是这样的:
1 | Got potato 4338641384... |
既然是异步的,在请求之后不一定要死等,而是可以做其他事情。比如除了土豆,我还想买番茄,这时只需要在事件循环中再添加一个过程:
1 | def main(): |
再来运行这段代码:
1 | Got potato 4423119312... |
看下AsyncGenerator的定义,它需要实现aiter和anext两个核心方法,以及asend,athrow,aclose方法。
1 | class AsyncGenerator(AsyncIterator): |
异步生成器是在3.6之后才有的特性,同样的还有异步推导表达式,因此在上面的例子中,也可以写成这样:
1 | bucket = [p async for p in take_potatos(50)] |
类似的,还有await表达式:
1 | result = [await fun() for fun in funcs if await condition()] |
除了函数之外,类实例的普通方法也能用async语法修饰:
1 | class ThreeTwoOne: |
实例方法的调用同样是返回一个coroutine:
1 | function = ThreeTwoOne.begin |
同理还有类方法:
1 | class ThreeTwoOne: |
根据PEP 492中,async也可以应用到上下文管理器中,aenter和aexit需要返回一个Awaitable:
1 | class GameContext: |
在3.7版本,contextlib中会新增一个asynccontextmanager装饰器来包装一个实现异步协议的上下文管理器:
1 | from contextlib import asynccontextmanager |
async修饰符也能用在call方法上:
1 | class GameContext: |
await和yield from
Python3.3的yield from语法可以把生成器的操作委托给另一个生成器,生成器的调用方可以直接与子生成器进行通信:
1 | def sub_gen(): |
利用这一特性,使用yield from能够编写出类似协程效果的函数调用,在3.5之前,asyncio正是使用@asyncio.coroutine和yield from语法来创建协程:
1 | # https://docs.python.org/3.4/library/asyncio-task.html |
然而,用yield from容易在表示协程和生成器中混淆,没有良好的语义性,所以在Python 3.5推出了更新的async/await表达式来作为协程的语法。
因此类似以下的调用是等价的:
1 | async with lock: |
那么,怎么把生成器包装为一个协程对象呢?这时候可以用到types包中的coroutine装饰器(如果使用asyncio做驱动的话,那么也可以使用asyncio的coroutine装饰器),@types.coroutine装饰器会将一个生成器函数包装为协程对象:
1 | import asyncio |
尽管两个函数分别使用了新旧语法,但他们都是协程对象,也分别称作native coroutine以及generator-based coroutine,因此不用担心语法问题。
下面观察一个asyncio中Future的例子:
1 | import asyncio |
两个协程在在事件循环中,协程coro1在执行第一句后挂起自身切到asyncio.sleep,而协程coro2一直等待future的结果,让出事件循环,计时器结束后coro1执行了第二句设置了future的值,被挂起的coro2恢复执行,打印出future的结果’data’。
future可以被await证明了future对象是一个Awaitable,进入Future类的源码可以看到有一段代码显示了future实现了await协议:
1 | class Future: |
当执行await future这行代码时,future中的这段代码就会被执行,首先future检查它自身是否已经完成,如果没有完成,挂起自身,告知当前的Task(任务)等待future完成。
当future执行set_result方法时,会触发以下的代码,设置结果,标记future已经完成:
1 | def set_result(self, result): |
最后future会调度自身的回调函数,触发Task._step()告知Task驱动future从之前挂起的点恢复执行,不难看出,future会执行下面的代码:
1 | class Future: |
最终返回结果给调用方。
前面讲了那么多关于asyncio的例子,那么除了asyncio,就没有其他协程库了吗?asyncio作为python的标准库,自然受到很多青睐,但它有时候还是显得太重量了,尤其是提供了许多复杂的轮子和协议,不便于使用。
你可以理解为,asyncio是使用async/await语法开发的协程库,而不是有asyncio才能用async/await,除了asyncio之外,curio和trio是更加轻量级的替代物,而且也更容易使用。
curio的作者是David Beazley,下面是使用curio创建tcp server的例子,据说这是dabeaz理想中的一个异步服务器的样子:
1 | from curio import run, spawn |
无论是asyncio还是curio,或者是其他异步协程库,在背后往往都会借助于IO的事件循环来实现异步,下面用几十行代码来展示一个简陋的基于事件驱动的echo服务器:
1 | from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR |
验证一下:
1 | # terminal 1 |
现在知道,完成异步的代码不一定要用async/await,使用了async/await的代码也不一定能做到异步,async/await是协程的语法糖,使协程之间的调用变得更加清晰,使用async修饰的函数调用时会返回一个协程对象,await只能放在async修饰的函数里面使用,await后面必须要跟着一个协程对象或Awaitable,await的目的是等待协程控制流的返回,而实现暂停并挂起函数的操作是yield。