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

本文参考了:html

yield 和 yield from

先让咱们来学习或者回顾一下yieldyield from的用法。若是你很自信本身完成理解了,能够跳到下一部分。python

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

yield from iterator
复制代码

本质上也就至关于:github

for x in iterator:
    yield x
复制代码

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

>>> 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:bash

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 的协程有了在总体上有了一个把握。

若是你像我同样真正热爱计算机科学,喜欢研究底层逻辑,欢迎关注个人微信公众号:

相关文章
相关标签/搜索