本文参考了:

  • How the heck does async/await work in Python 3.5?
  • PEP 380: Syntax for Delegating to a Subgenerator

yield 和 yield from

先让我们来学习或者回顾一下 yieldyieldfrom的用法。如果你很自信自己完成理解了,可以跳到下一部分。

Python3.3提出了一种新的语法: yieldfrom

yield from iterator

本质上也就相当于:

for x in iterator:
yield x

下面的这个例子中,两个 yieldfrom加起来,就组合得到了一个大的 iterable(例子来源于官网3.3 release):

>>> def g(x):
... yield from range(x, 0, -1)
... yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

理解 yieldfrom对于接下来的部分至关重要。想要完全理解 yieldfrom,还是来看看官方给的例子:

def accumulate():
tally = 0
while 1:
next = yield
if next is None:
return tally
tally += next

def gather_tallies(tallies):
while 1:
tally = yield from accumulate()
tallies.append(tally)

tallies = []
acc = gather_tallies(tallies)
next(acc) # Ensure the accumulator is ready to accept values

for i in range(4):
acc.send(i)
acc.send(None) # Finish the first tally

for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally
print(tallies)

我还专门为此录制了一段视频,你可以配合文字一起看,或者你也可以打开 pycharm 以及任何调试工具,自己调试一下。 视频链接

来一起 break down:

acc=gather_tallies(tallies)这一行开始,由于 gather_tallies函数中有一个 yield,所以不会 while1立即执行(你从视频中可以看到,acc 是一个 generator 类型)。

next(acc)

next()会运行到下一个 yield,或者报StopIteration错误。

next(acc)进入到函数体gathertallies,gathertallies中有一个 yieldfromaccumulate(),next(acc)不会在这一处停,而是进入到『subgenerator』accumulate里面,然后在 next=yield处,遇到了 yield,然后暂停函数,返回。

for i in range(4):
acc.send(i)

理解一下 acc.send(value)有什么用:

  • 第一步:回到上一次暂停的地方
  • 第二步:把value 的值赋给 xxx=yield 中的 xxx,这个例子中就是 next

accumulate函数中的那个while 循环,通过判断 next的值是不是 None 来决定要不要退出循环。在 foriinrange(4)这个for循环里面,i 都不为 None,所以 while 循环没有断。但是,根据我们前面讲的:next()会运行到下一个 yield的地方停下来,这个 while 循环一圈,又再次遇到了 yield,所以他会暂停这个函数,把控制权交还给主线程。

理清一下:对于accumulate来说,他的死循环是没有结束的,下一次通过 next()恢复他运行时,他还是在运行他的死循环。对于gather_tallies来说,他的 yieldfromaccumulate()也还没运行完。对于整个程序来说,确实在主进程和accumulate函数体之间进行了多次跳转。

接下来看第一个 acc.send(None):这时 next变数的值变成了 NoneifnextisNone条件成立,然后返回 tally给上一层函数。(计算一下,tally 的值为0 + 1 + 2 + 3 = 6)。这个返回值就赋值给了 gather_tallies中的 gally。这里需要注意的是, gather_tallies的死循环还没结束,所以此时调用 next(acc)不会报 StopIteration错误。

for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally

这一部分和前面的逻辑是一样的。acc.send(i)会先进入 gather_tallies,然后进入 accumulate,把值赋给 nextacc.send(None)停止循环。最后tally的值为10(0 + 1 + 2 + 3 + 4)。

最终tallies列表为: [6,10]

Python async await发展简史

看一下 wikipedia 上 Coroutine的定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.

关键点在于by allowing execution to be suspended and resumed.(让执行可以被暂停和被恢复)。通俗点说,就是:

coroutines are functions whose execution you can pause。(来自How the heck does async/await work in Python 3.5?)

这不就是生成器吗?

python2.2 - 生成器起源

Python生成器的概念最早起源于 python2.2(2001年)时剔除的 pep255,受Icon 编程语言启发。

生成器有一个好处,不浪费空间,看下面这个例子:

def eager_range(up_to):
"""Create a list of integers, from 0 to up_to, exclusive."""
sequence = []
index = 0
while index < up_to:
sequence.append(index)
index += 1
return sequence

如果用这个函数生成一个10W 长度的列表,需要等待 while 循环运行结束返回。然后这个 sequence列表将会占据10W 个元素的空间。耗时不说(从能够第一次能够使用到 sequence 列表的时间这个角度来看),占用空间还很大。

借助上一部分讲的 yield,稍作修改:

def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
while index < up_to:
yield index
index += 1

这样就只需要占据一个元素的空间了,而且立即就可以用到 range,不需要等他全部生成完。

python2.5 : send stuff back

一些有先见之明的前辈想到,如果我们能够利用生成器能够暂停的这一特性,然后想办法添加 send stuff back 的功能,这不就符合维基百科对于协程的定义了么?

于是就有了pep342。

pep342中提到了一个 send()方法,允许我们把一个"stuff"送回生成器里面,让他接著运行。来看下面这个例子:

def jumping_range(up_to):
"""Generator for the sequence of integers from 0 to up_to, exclusive.

Sending a value into the generator will shift the sequence by that amount.
"""
index = 0
while index < up_to:
jump = yield index
if jump is None:
jump = 1
index += jump

if __name__ == __main__:
iterator = jumping_range(5)
print(next(iterator)) # 0
print(iterator.send(2)) # 2
print(next(iterator)) # 3
print(iterator.send(-1)) # 2
for x in iterator:
print(x) # 3, 4

这里的 send把一个『stuff』送进去给生成器,赋值给 jump,然后判断jump 是不是 None,来执行对应的逻辑。

python3.3 yield from

自从Python2.5之后,关于生成器就没做什么大的改进了,直到 Python3.3时提出的pep380。这个 pep 提案提出了 yieldfrom这个可以理解为语法糖的东西,使得编写生成器更加简洁:

def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
def gratuitous_refactor():
nonlocal index
while index < up_to:
yield index
index += 1
yield from gratuitous_refactor()

第一节我们已经详细讲过 yield from 了,这里就不赘述了。

python3.4 asyncio模块

插播:事件循环(eventloop)

如果你有 js 编程经验,肯定对事件循环有所了解。

理解一个概念,最好也是最有bigger的就是翻出 wikipedia:

an event loop "is a programming construct that waits for and dispatches events or messages in a program" - 来源于Event loop - wikipedia

简单来说,eventloop 实现当 A 事件发生时,做 B 操作。拿浏览器中的JavaScript事件循环来说,你点击了某个东西(A 事件发生了),就会触发定义好了的 onclick函数(做 B 操作)。

在 Python 中,asyncio 提供了一个 eventloop(回顾一下上一篇的例子),asyncio 主要聚焦的是网路请求领域,这里的『A 事件发生』主要就是 socket 可以写、 socket可以读(通过 selectors模块)。

到这个时期,Python 已经通过 Concurrentprogramming的形式具备了非同步编程的实力了。

Concurrentprogramming只在一个 thread 里面执行。go 语言blog 中有一个非常不错的视频:Concurrency is not parallelism,很值得一看。

这个时代的 asyncio 代码

这个时期的asyncio代码是这样的:

import asyncio

# Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.
@asyncio.coroutine
def countdown(number, n):
while n > 0:
print(T-minus, n, ({}).format(number))
yield from asyncio.sleep(1)
n -= 1

loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(countdown("A", 2)),
asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出结果为:

T-minus 2 (A)
T-minus 3 (B)
T-minus 1 (A)
T-minus 2 (B)
T-minus 1 (B)

这时使用的是 asyncio.coroutine修饰器,用来标记某个函数可以被 asyncio 的事件循环使用。

看到 yieldfromasyncio.sleep(1)了吗?通过对一个asyncio.Future object yieldfrom,就把这个future object 交给了事件循环,当这个 object 在等待某件事情发生时(这个例子中就是等待 asyncio.sleep(1),等待 1s 过后),把函数暂停,开始做其他的事情。当这个future object 等待的事情发生时,事件循环就会注意到,然后通过调用 send()方法,让它从上次暂停的地方恢复运行。

break down 一下上面这个代码:

事件循环开启了两个 countdown()协程调用,一直运行到 yieldfromasyncio.sleep(1),这会返回一个 future object,然后暂停,接下来事件循环会一直监视这两个future object。1秒过后,事件循环就会把 future object send()给coroutine,coroutine又会接著运行,列印出 T-minus2(A)等。

python3.5 async await

python3.4的

@asyncio.coroutine
def py34_coro():
yield from stuff()

到了 Python3.5,可以用一种更加简洁的语法表示:

async def py35_coro():
await stuff()

这种变化,从语法上面来讲并没什么特别大的区别。真正重要的是,是协程在 Python 中哲学地位的提高。 在 python3.4及之前,非同步函数更多就是一种很普通的标记(修饰器),在此之后,协程变成了一种基本的抽象基础类型(abstract base class):class collections.abc.Coroutine。

How the heck does async/await work in Python 3.5?一文中还讲到了 asyncawait底层 bytecode 的实现,这里就不深入了,毕竟篇幅有限。

把 async、await看作是API 而不是 implementation

Python 核心开发者(也是我最喜欢的 pycon talker 之一)David M. Beazley在PyCon Brasil 2015的这一个演讲中提到:我们应该把 asyncawait看作是API,而不是实现。 也就是说, asyncawait不等于 asyncioasyncio只不过是 asyncawait的一种实现。(当然是 asyncio使得非同步编程在 Python3.4中成为可能,从而推动了 asyncawait的出现)

他还开源了一个项目github.com/dabeaz/curio,底层的事件循环机制和 asyncio 不一样, asyncio使用的是 futureobjectcurio使用的是 tuple。同时,这两个 library 有不同的专注点, asyncio 是一整套的框架, curio则相对更加轻量级,用户自己需要考虑到事情更多。

How the heck does async/await work in Python 3.5?此文还有一个简单的事件循环实现例子,有兴趣可以看一下,后面有时间的话也许会一起实现一下。

总结一下

  • 协程只有一个 thread。
  • 操作系统调度进程、协程用事件循环调度函数。
  • async、await 把协程在 Python 中的哲学地位提高了一个档次。

最重要的一点感受是:Nothing is Magic。现在你应该能够对 Python 的协程有了在整体上有了一个把握。

如果你像我一样真正热爱计算机科学,喜欢研究底层逻辑,欢迎关注我的微信公众号:


推荐阅读:
相关文章