前言

很久以前就听说 Python 的 async/await 很厉害,但是直到现在都没有用过,一直都在用多线程模型来解决各种问题。最近看到隔壁的 Go 又很火,所以决定花时间研究下 Python 协程相关的内容,终于在翻阅了一裤衩的资料之后有了一些理解。

起:一切从生成器开始

以往在 Python 开发中,如果需要进行并发编程,通常是使用多线程 / 多进程模型实现的。由于 GIL 的存在,多线程对于计算密集型的任务并不十分友好,而对于 IO 密集型任务,可以在等待 IO 的时候进行线程调度,让出 GIL,实现『假并发』。

当然对于 IO 密集型的任务另外一种选择就是协程,协程其实是运行在单个线程中的,避免了多线程模型中的线程上下文切换,减少了很大的开销。为了理解协程、async/await、asyncio,我们要从最古老的生成器开始。

回顾 Python 的历史,生成器这个概念第一次被提出的时候是在PEP 255中被提出的,当时的 Python 版本为 Python2.2。我们都知道range()函数,现在考虑一下我们来编写一个自己的range()函数,最直接最容易想到的方法也许是这样:

当你想创建一个很小的序列的时候,例如创建从 0 到 100 这样的列表,似乎没什么问题。但是如果想创建一个从 0 到 999999999 这么大的列表的话,就必须要创建一个完整的,长度是 999999999 的列表,这个行为非常占用内存。于是就有了生成器,用生成器来改写这个函数的话,会是下面这个样子:

当函数执行遇到yield的时候,会暂停执行。这样只需在内存中维护可以存储一个整数的内存空间就可以了。

承:协程诞生

到这里可能还和协程没什么关系,但是实际上这已经是 Python 协程的雏形了,我们来看看维基上对于协程的定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing

multiple entry points for suspending and resuming execution at certain locations.

从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行。那么yield已经可以暂停执行了,如果在暂停后有办法把一些 value 发回到暂停执行的函数中,那么 Python 就有了『协程』。于是在PEP 342中,添加了 “把东西发回已经暂停的生成器中” 的方法,这个方法就是send(),并且在 Python2.5 中得到了实现。利用这个特性我们继续改写

range()函数:

就这样,整个生成器的部分似乎已经进入了stable的状态,但是在 Python3.3 中,这个情况发生了改变。在PEP 380中,为 Python3.3 添加了yield from,这个东西可以让你从迭代器中返回任何值(这里用的是迭代器,因为生成器也是一种迭代器),也可以让你重构生成器,我们来看这个例子:

这个特性也可以让生成器进行串联,使数据在多个生成器中进行传递。历史发展到这里,协程的出现似乎已经就差一步了,或者这里说是异步编程更恰当。在 Python3.4 中加入了 asyncio 库,使 Python 获得了事件循环的特性(关于事件循环的内容这里不再赘述)。asyncio + 生成器已经达到了异步编程的条件,在 Python3.4 中,我们就可以这样实现一个异步的模型:

这里的asyncio.coroutine装饰器是用来标记这个函数是一个协程的,因为asyncio要求所有要用作协程的生成器必须由asyncio.coroutine装饰。在这段代码中,时间循环会启动两个countdown()协程,他们会一直执行,直到遇到了yield from asyncio.sleep(),会暂停执行,并且将一个asyncio.Future对象返回给事件循环。事件循环会监控这个asyncio.Future对象,一旦其执行完成后,将会把这个 Future 的执行结果返回给刚刚因为这个 Future 暂停的协程,并且继续执行原协程。

从这个具体的例子出发,抽象点来看,实际上这个过程变成了:

你可以对任何asyncio.Future的对象进行yield from,将这个 Future 对象交给事件循环;

暂停执行的协程将等待这个 Future 的完成;

一旦 Future 获取到事件循环,并执行完所有的代码;

事件循环感知到 Future 执行完毕,原暂停的协程会通过 send() 方法获取 Future 对象的返回值并且继续执行;

转:从 yield from 到 await

终于到了最激动人心的地方,在 Python3.5 中,添加了types.coroutine装饰器以及async def和await。我们先来看一下 Python3.4 和 Python3.5 中如何定义一个协程函数:

看起来 Python3.5 中定义协程更为简单了,但是实际上生成器和协程之间的差别变的更加明显了。这里先要指出两个个注意点:

await 只能用于 async def 的函数中;

yield from 不能用于 async def 的函数中;

除此之外,yield from和await可以接受的对象是略有区别的,await接受的对象必须是一个awaitable对象。什么是awaitable对象呢,就是一个实现了__await()__方法的对象,而且这个方法必须返回一个不是协程的迭代器。满足这两个条件,才算是一个awaitable对象,当然协程本身也是awaitable对象,因为collections.abc.Coroutine继承了collections.abc.Awaitable。换句话说,await后面可接受的对象有两种,分别是:协程和awaitable对象,当然协程也是awaitable对象。

在 Python3.6 中,这种特性继续被发扬光大,现在可以在同一个函数体内使用yield和await,而且除此之外,也可以在列表推导等地方使用async for或await语法。

尾声

到这里整个协程的历史已经是回顾完了,对于 Python 中的协程也有了一些理解!