Py学习  »  Python

异步编程 101:Python async await发展简史

liaochangjiang • 5 年前 • 359 次点击  
阅读 76

异步编程 101:Python async await发展简史

本文参考了:

yield 和 yield from

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

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

yield from iterator
复制代码

本质上也就相当于:

for x in iterator:
    yield x
复制代码

下面的这个例子中,两个 yield from加起来,就组合得到了一个大的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]
复制代码

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

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,所以不会while 1立即执行(你从视频中可以看到,acc 是一个 generator 类型)。

next(acc)

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

next(acc)进入到函数体gather_tallies,gather_tallies中有一个yield from accumulate(),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 来决定要不要退出循环。在for i in range(4)这个for循环里面,i 都不为 None,所以 while 循环没有断。但是,根据我们前面讲的:next()会运行到下一个 yield的地方停下来,这个 while 循环一圈,又再次遇到了yield,所以他会暂停这个函数,把控制权交还给主线程。

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

接下来看第一个acc.send(None):这时next变量的值变成了Noneif next is None条件成立,然后返回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 提案提出了yield from这个可以理解为语法糖的东西,使得编写生成器更加简洁:

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 已经通过Concurrent programming的形式具备了异步编程的实力了。

Concurrent programming只在一个 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 的事件循环使用。

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

break down 一下上面这个代码:

事件循环开启了两个countdown()协程调用,一直运行到yield from asyncio.sleep(1),这会返回一个 future object,然后暂停,接下来事件循环会一直监视这两个future object。1秒过后,事件循环就会把 future object send()给coroutine,coroutine又会接着运行,打印出T-minus 2 (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/curi…,底层的事件循环机制和 asyncio 不一样,asyncio使用的是future objectcurio使用的是tuple。同时,这两个 library 有不同的专注点,asyncio 是一整套的框架,curio则相对更加轻量级,用户自己需要考虑到事情更多。

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

总结一下

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

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

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

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/32296
 
359 次点击