Python async/await 入门

在新版 Python3.5 中,引入了两个新关键字 async 和 await,用于解决在 Python 异步编程中无法有效 区分 yield 生成器与异步的关系的问题。

异步是一个什么东西

异步的作用在于,对于 Python 这种拥有 GIL 的语言,某个线程在处理单个耗时较长的任务时(如 I/O 读取,RESTful API 调用)等操作时,不能有效的释放 CPU 资源,导致其他线程的等待时间随之增加。 异步的作用是,在等待这种花费大量时间的操作数,允许释放 CPU 资源执行其他的线程任务,从而提 高程序的执行效率。

3.4 之前如何实现异步

在 3.5 版本以前的程序中,Python 程序通常是使用 yield 作为一个判断是否进入异步操作的关键词。 比如在 3.4.x 版本中,我们可以用这样的一个例子来看一下 (或者你也可以用一个 Tornado 的例子,这 样你的程序就也可以运行在 2.7.x 版本的 Python 中了):

import time
import asyncio

@asyncio.coroutine
def slow_operation(n):
    yield from asyncio.sleep(1)
    print("Slow operation {} complete".format(n))


@asyncio.coroutine
def main():
    start = time.time()
    yield from asyncio.wait([slow_operation(1),
        slow_operation(2),
        slow_operation(3),
    ])
    end = time.time()
    print('Complete in {} second(s)'.format(end-start))


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

执行结果如下:

➜  Desktop  pyenv shell 3.4.3
➜  Desktop  python 3.4_asyncio.py
➜  Desktop  python 3.4_asyncio.py
Slow operation 2 complete
Slow operation 1 complete
Slow operation 3 complete
Complete in 1.0008249282836914 second(s)

如果你了解过 yield,你会知道 yield 其实作用是用来生成一个生成器,而生成器的作用是用于输出一系列的结果。比如计算斐波那契数列:

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1

for n in fab(5):
    print n

其实这个在实际执行过程中,生成器并未实际执行,只有当调用. next() 时才开始执行,并返回当前的迭代值。最后执行完成之后,则会抛出 StopIteration 异常,表示迭代完成。当然,这个异常在大多数循环情况下 (比如 for) 并不需要手工处理。当然,你也可以选择使用下面手工的方法(注意:next 是 Python3 中的内置函数,如果是 Python2,请使用 f.next()):

➜  Desktop  python
Python 3.4.3 (default, Aug 14 2015, 11:21:11)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def fab(max):
...     n, a, b = 0, 0, 1
...     while n < max:
...         yield b
...         a, b = b, a + b
...         n = n + 1
...
>>> f = fab(5)
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

上面的方法如果不使用 yield,则换成一个支持 iterable 的可能更直观理解一点:

 class Fab(object):

    def __init__(self, max):
        self.max = max
        self.n, self.a, self.b = 0, 0, 1

    def __iter__(self):
        return self

    def next(self):
        if self.n < self.max:
            r = self.b
            self.a, self.b = self.b, self.a + self.b
            self.n = self.n + 1
            return r
        raise StopIteration()

在异步中,程序的 ioloop 会自动处理这一类的生成器,这一个样例可以参考一下 Tornado 的处理方式: tornado.gen.coroutine

3.5 版本异步功能

在 3.5 之后的版本里,程序提供了 async 和 await 关键字,这两个关键字可以替代类似 tornado 中的 gen.coroutine/yield 或者是 asyncio.coroutine/yield 的作用。

比如在上一节中的例子改造成为:

import asyncio
import time

async def slow_operation(n):
    await asyncio.sleep(1)
    print("Slow operation {} complete".format(n))


async def main():
    start = time.time()
    await asyncio.wait([slow_operation(1),
        slow_operation(2),
        slow_operation(3),
    ])
    end = time.time()
    print('Complete in {} second(s)'.format(end-start))


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

新的关键字会让我们的程序看上去更加清晰。不过从执行效率上看,现在还是相较 3.4.3 有一些退步:

➜  Desktop  pyenv shell 3.5.0rc1
➜  Desktop  python 3.5_asyncio.py
Slow operation 1 complete
Slow operation 3 complete
Slow operation 2 complete
Complete in 1.0012218952178955 second(s)
➜  Desktop  python 3.4_asyncio.py
Slow operation 1 complete
Slow operation 3 complete
Slow operation 2 complete
Complete in 1.0060911178588867 second(s)

不过现在还只是 RC 版本,相信之后还是会有一些性能调优的可能性。

更多的开发场景

现在 Tornado 正在开发的 4.3 版本已经支持了 Python3.5 的 async/await 关键字,在之后的开发中,可以替代 @gen.coroutine 和 yield 用于开发。根据作者的描述,如果是运行在 3.5 版本的 Python 上,建议使用关键字而不是 yield 方式,这样可以有更好的执行效率。当然,如果 3.5 只是一个可选版本,相信在相当长的一段时间之内,你还是需要使用原有的执行方式。

潜在的坑

async 和 await 在 Python 3.5 与 3.6 版本中并不是关键字,Python 3.7 中会作为关键字。因此需要在现有的程序中替代掉已有的可能导致冲突的关键字。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy