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 中了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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())

执行结果如下:

1
2
3
4
5
6
7
➜  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 其实作用是用来生成一个生成器,而生成器的作用是用于输出一系列的结果。比如计算斐波那契数列:

1
2
3
4
5
6
7
8
9
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()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
➜  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 的可能更直观理解一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的作用。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 有一些退步:

1
2
3
4
5
6
7
8
9
10
11
➜  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 中会作为关键字。因此需要在现有的程序中替代掉已有的可能导致冲突的关键字。