协程的演变

其实早在 Python3.4 的时候就有协程,当时的协程是通过 @asyncio.coroutine 和 yeild from 实现的。在一些很老教程中你可能看到的是下面这种形式:

import asyncio
@asyncio.coroutine
def print_hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")
# 创建并获取EventLoop:
loop = asyncio.get_event_loop()
# 执行协程
loop.run_until_complete(print_hello())
loop.close()

因为现在几乎没有人这样写了,所以仅作为了解即可。

然后到了 Python3.5 引入了 async/await 语法糖,一直到现在Python3.8 都是用这种形式来表示协程,示例如下。

import asyncio
async def print_hello():
print("Hello world!")
await asyncio.sleep(1)
print("Hello again!")
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
print("开始运行协程")
coro = print_hello()
print("进入事件循环")
loop.run_until_complete(coro)
finally:
print("关闭事件循环")
loop.close()

这种是目前应用范围最广的,可以看到比之前的代码舒服了不少,不用再使用装饰器的形式了。

然后就到了 Python3.7 和 Python3.8,协程发生了很多细小的变化,但是最大的一个变化就是,启动协程的方法变简单了,一句就可以搞定,不用再像上面那样,创建循环然后再仍到事件循环去执行。使用 asyncio.run这个顶级 API 就可以了。

import asyncio
async def print_hello():
print("Hello world!")
await asyncio.sleep(1)
print("Hello again!")
if __name__ == '__main__':
print("开始运行协程")
asyncio.run(print_hello())
print("进入事件循环")

怎么样是不是代码更少了,启动协程更简单了。所以这也正是我们使用 3.8 作为本教程的 Python 版,与时俱进嘛。

asyncio 的组成部分

根据目前的官方文档,总的来说分为了两部分:高层级 API 和低层级 API。

首先看高层级 API 也是接下来重点要讲的。

高层级 API

协程对象和 Tasks 对象

数据流

同步源语

子进程

队列

异常

低层级 API

事件循环

Futures 对象

传输和协议

策略

平台支持

上面列出了这么多的项目我们怎么去选择自己所需要的呢,总的来说对于刚入门的新手或者只是写一个自己用的程序一般都只会用到高级 API 的部分,这部分就属于开箱即用的那种,对于高级用户比如框架开发者,往往可以需要去适应各种需要,需要重新改写一些内部的结构,这个时候就需要用到低层级的 API,但是这两个层级呢只能是一个大概方向吧,主要是方便 API 的查看,下面呢我将围绕者高层级 API 和低层级 API 在日常实际工作中经常用到的内容做一些讲解。

概念

在学习 asyncio 之前需要知道这样的几个概念。

事件循环

事件循环是一种处理多并发量的有效方式,在维基百科中它被描述为「一种等待程序分配事件或消息的编程架构」,我们可以定义事件循环来简化使用轮询方法来监控事件,通俗的说法就是「当 A 发生时,执行 B」。所谓的事件,其实就是函数。事件循环,就是有一个队列,里面存放着一堆函数,从第一个函数开始执行,在函数执行的过程中,可能会有新的函数继续加入到这个队列中。一直到队列中所有的函数被执行完毕,并且再也不会有新的函数被添加到这个队列中,程序就结束了。

Future

Future 是一个数据结构,表示还未完成的工作结果。事件循环可以监视 Future 对象是否完成。从而允许应用的一部分等待另一部分完成一些工作。

简单说,Future 就是一个类,用生成器实现了回调。

Task

Task 是 Future 的一个子类,它知道如何包装和管理一个协程的执行。任务所需的资源可用时,事件循环会调度任务允许,并生成一个结果,从而可以由其他协程消费。一般操作最多的还是 Task。用 Task 来封装协程,给原本没有状态的协程增加一些状态。

awaitable objects(可等待对象)

如果一个对象可以用在 wait 表达式中,那么它就是一个可等待的对象。在 asyncio 模块中会一直提到这个概念,其中协程函数,Task,Future 都是 awaitable 对象。

用于 await 表达式中的对象。可以是 coroutine 也可以是实现了 await() 方法的对象,参见 PEP 492。类比于 Iterable 对象是 Generator 或实现了 iter() 方法的对象。

object._await_(self)

必须返回生成器,asyncio.Future 类也实现了该方法,用于兼容 await 表达式。

而 Task 继承自 Future,因此 awaitable 对象有三种:coroutines、Tasks 和 Futures。

await 的目的:

获取协程的结果

挂起当前协程,将控制交由事件循环,切换到其他协程,然后等待结果,最后恢复协程继续执行

启动一个协程

现在我们使用 async/await 语法来声明一个协程。 代码如下:

import asyncio
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
if __name__ == '__main__':
asyncio.run(main())

asyncio.run 只能用来启动程序入口协程,反过来你在程序中如果使用 asyncio.run 就会出错,之前我们提到对于其他的协程通过 await 链来实现,这里也是一样的。

下面说下代码的含义,首先启动 main 这个协程,main 方法就是先打印 hello,然后在打印过程中通过使用 asyncio.sleep 来等待 1 秒,之后再打印 world。前面我们提到用协程就意味着我们要一直使用非阻塞的代码,才能达到速度提升,所以这里我们用了非阻塞版的 time.sleep 即 asyncio.sleep 。

协程中调用协程

之前我们提到了在协程中,可以使用 await 来调用一个协程。

就像下面的代码:

import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
#使用f-string拼接字符串
print(f"开始运行 {time.strftime('%X')}")
child1=await say_after(1, 'hello') #通过await调用协程,然后接收一下返回值
child2=await say_after(2, 'world')
print("child1",child1)
print("child2",child2)
print(f"结束运行 {time.strftime('%X')}")
if __name__ == '__main__':
asyncio.run(main())
运行结果:
开始运行 11:17:26
hello
world
child1 None
child2 None
结束运行 11:17:29
[Finished in 3.1s]

代码是没什么问题,正常运行。但是一般情况下我们用到更多的是下面的方式。将协程封装为 Task 让原本没有状态标示的协程添加上状态 。

我们可以通过 asyncio.create_task 方法来实现。

asyncio.create_task

create_task(在 3.6 版本中需要使用低层级的 API asyncio.ensure_future。)是 3.7以后加入的语法,作用是将协程包装为一个任务(Task),相比 3.6 版本的 ensure_future 可读性提高。

将上面的代码做如下修改:

import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"开始运行 {time.strftime('%X')}")
child1=asyncio.create_task(say_after(1, 'hello')) #通过await调用协程,然后接收一下返回值
child2=asyncio.create_task(say_after(2, 'world'))
print("调用任务child1前",child1)
print("调用任务child2前",child2)
await child1
await child2
print("调用任务child1后",child1)
print("调用任务child2前",child2)
print(f"结束运行 {time.strftime('%X')}")
if __name__ == '__main__':
asyncio.run(main())
运行结果如下:
开始运行 11:37:54
调用任务child1前 >
调用任务child2前 >
hello
world
调用任务child1后 result=None>
调用任务child2前 result=None>
结束运行 11:37:56

可以发现,我们的结果中多了 "

前面说到 Task 是 Future 的子类,所以 Task 拥有 Future 的一些状态。

Future 的状态

大概有如下几种: