[Python] async def & await 重点整理

最近实习要用到 FastAPI,我发现 FastAPI 的 path operation function 会使用 async def,还会搭配使用 await,因为对这两个关键字没很熟,所以就藉机纪录一下,也避免之后忘记。

async def & await 使用情境

我直接利用下面这个例子来展示什么情况下可以使用 asyncawait

import timedef dosomething(i):    print(f"第 {i} 次开始")    time.sleep(2)    print(f"第 {i} 次结束")if __name__ == "__main__":    start = time.time()    for i in range(5):        dosomething(i+1)    print(f"time: {time.time() - start} (s)")

执行后应该会像这样。

第 1 次开始第 1 次结束第 2 次开始第 2 次结束第 3 次开始第 3 次结束第 4 次开始第 4 次结束第 5 次开始第 5 次结束time: 10.048049688339233 (s)

这非常直觉,因为每次呼叫 dosomething() 时都会等待2秒,等完才会执行下一轮,所以最后执行总时间是10秒相当合理。

但仔细想想,如果那2秒是做网路请求或档案读写(IO),这2秒是不需要CPU的,但CPU就只能发呆2秒,痴痴地等待回传结果,其他什么事都不能做,岂不是太浪费了吗!? (学过作业系统的人就知道,绝对不能让CPU发呆XD)

因此 Python 就有了 asyncio 这个工具,来彻底的利用(X) 榨乾(O) CPU的效能。

我把刚才的例子改成 asyncio 的版本。

import timeimport asyncioasync def dosomething(i):    print(f"第 {i} 次开始")    await asyncio.sleep(2)    print(f"第 {i} 次结束")if __name__ == "__main__":    start = time.time()    tasks = [dosomething(i+1) for i in range(5)]    asyncio.run(asyncio.wait(tasks))    print(f"time: {time.time() - start} (s)")

执行结果会变成这样,只需要2秒就结束了!

第 2 次开始第 1 次开始第 3 次开始第 4 次开始第 5 次开始第 2 次结束第 3 次结束第 5 次结束第 1 次结束第 4 次结束time: 2.011152982711792 (s)

为什么会这样呢? 其实 await 就是告诉 CPU 说后面这个函数很慢,不需要等它执行完毕。因此此时 CPU 就可以先跳去执行其他的事情,只需要在这个函数结束时再回来处理就好。这就是为什么速度会快很多。

了解 async def & await 使用情境之后,就来说明一些细节吧!

coroutine

首先,async def & await 是 Python 3.5+ 之后才出现的 语法糖,目的是让 coroutine 之间的调度更加清楚。

那就要先了解什么是 coroutine。

根据 Python 官方对 coroutine 定义:

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the async def statement.

简单讲 coroutine 可以在任意的时间点开始、暂停和离开,并且透过 async def 宣告此函数为一个 coroutine。

所以 await 的作用就是告诉 CPU 说可以暂停后面的工作,先去执行其他程式。另外 await 只能在 coroutine 中宣告,这就是为什么 await 必须写在 async def 里面。

另一个要注意的点,await 后只能接 awaitables 物件,awaitables 物件就包括 coroutine, Task, Future 和有实作 __await__() 的物件。所以并不是所有函数都可以使用 await 加速。

coroutine 使用範例

最后来讲 coroutine 的使用範例吧!

import asyncioasync def main():    await asyncio.sleep(1)    print('hello')main()

执行后应该会出错:

RuntimeWarning: coroutine 'main' was never awaited  main()RuntimeWarning: Enable tracemalloc to get the object allocation traceback

这是因为现在 main() 已经宣告成一个 coroutine 了,所以不能够直接呼叫,而是要改成用 asyncio.run() 呼叫,所以将程式码改成下面这样就可以成功印出 hello 了。

import asyncioasync def main():    await asyncio.sleep(1)    print('hello')asyncio.run(main())

好,async def & await 就大致介绍到这边,关于 asyncio 有满多东西可以玩的,有需要欢迎看这篇 Python asyncio 从不会到上路。

FastAPI async def & await

最后回来看 FastAPI 文件 中怎么说明 async def & await 的。

他说如果你需要在 path operation function 中呼叫一些很慢的函数 (如: 读取资料库、网路请求...) 时,就可以使用 async defawait 来加速,就像下面的例子。

@app.get('/')async def read_results():    results = await do_something()  # do_something() is slow    return results

但如果不需要呼叫这些函数,就直接使用一般 def 即可。

现在就能明白为何 FastAPI 要使用 async defawait 了吧!

参考资料

Concurrency and async / awaitPython asyncio 从不会到上路【Python教学】浅谈 Coroutine 协程使用方法【Python教学】Async IO Design Patterns 範例程式async await 学习笔记

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章