在FastAPI网站学python:Python 并发 async / await
python并发async / await,结合FastAPI手册进行学习:https://fastapi.tiangolo.com/zh/async/
快速上手async / await
如果正在使用第三方库,它们会告诉你使用 await
关键字来调用它们,就像这样:
results = await some_library()
然后,通过 async def
声明你的 路径操作函数:
@app.get('/')
async def read_results():results = await some_library()return results
这时只能在被 async def
创建的函数内使用 await
如果正在使用一个第三方库和某些组件(比如:数据库、API、文件系统...)进行通信,第三方库又不支持使用 await
(目前大多数数据库三方库都是这样),这种情况你可以像平常那样使用 def
声明一个路径操作函数,就像这样:
@app.get('/')
def results():results = some_library()return results
如果应用程序不需要与其他任何东西通信而等待其响应,请使用 async def
。
如果不清楚,使用 def
就好.
注意:可以根据需要在路径操作函数中混合使用 def
和 async def
,并使用最适合的方式去定义每个函数。FastAPI 将为他们做正确的事情。
无论如何,在上述任何情况下,FastAPI 仍将异步工作,速度也非常快。
但是,通过遵循上述步骤,它将能够进行一些性能优化。
异步技术细节
Python 的现代版本支持通过一种叫“协程”——使用 async
和 await
语法的东西来写”异步代码“。
让我们在下面的部分中逐一介绍:
- 异步代码
async
和await
- 协程
异步代码¶
异步代码仅仅意味着编程语言 💬 有办法告诉计算机/程序 🤖 在代码中的某个点,它 🤖 将不得不等待在某些地方完成一些事情。让我们假设一些事情被称为 "慢文件"📝.
所以,在等待"慢文件"📝完成的这段时间,计算机可以做一些其他工作。
然后计算机/程序 🤖 每次有机会都会回来,因为它又在等待,或者它 🤖 完成了当前所有的工作。而且它 🤖 将查看它等待的所有任务中是否有已经完成的,做它必须做的任何事情。
接下来,它 🤖 完成第一个任务(比如是我们的"慢文件"📝) 并继续与之相关的一切。
这个"等待其他事情"通常指的是一些相对较慢(与处理器和 RAM 存储器的速度相比)的 I/O 操作,比如说:
- 通过网络发送来自客户端的数据
- 客户端接收来自网络中的数据
- 磁盘中要由系统读取并提供给程序的文件的内容
- 程序提供给系统的要写入磁盘的内容
- 一个 API 的远程调用
- 一个数据库操作,直到完成
- 一个数据库查询,直到返回结果
- 等等.
这个执行的时间大多是在等待 I/O 操作,因此它们被叫做 "I/O 密集型" 操作。
它被称为"异步"的原因是因为计算机/程序不必与慢任务"同步",去等待任务完成的确切时刻,而在此期间不做任何事情直到能够获取任务结果才继续工作。
相反,作为一个"异步"系统,一旦完成,任务就可以排队等待一段时间(几微秒),等待计算机程序完成它要做的任何事情,然后回来获取结果并继续处理它们。
对于"同步"(与"异步"相反),他们通常也使用"顺序"一词,因为计算机程序在切换到另一个任务之前是按顺序执行所有步骤,即使这些步骤涉及到等待。
比如在统筹方法一文中,有一个华罗庚烧水泡茶的例子
烧水泡茶方法1
1、洗水壶:需要1分钟 2、烧开水:需要15分钟 3、洗茶壶:需要1分钟。4、洗茶杯:需要2分钟 5、拿茶叶:需要1分钟。6、泡茶
这样一共需要20分钟
烧水泡茶方法2
- 洗水壶:需要1分钟。
- 烧开水:需要15分钟。在此期间可以同时进行以下工作:
- 洗茶壶:需要1分钟。
- 洗茶杯:需要2分钟。
- 拿茶叶:需要1分钟。
- 泡茶:等水开后,泡茶完成。
这样通过统筹安排,总时间可以缩短到16分钟,而不是传统的20分钟。这是华罗庚在统筹方法一文中举的例子。方法2就是异步执行,方法1就是同步、顺序执行
并行处理
如果多个人同时进行泡茶的工作,就是并行处理。我们日常生活中就有很多并行处理的例子,比如银行有多个窗口,火车、地铁有多个车门,植树节大家一起植树,饭店多个炉灶同时做菜等。
现在计算机有多个核心,我们完全可以让它们同时做一些事情,这就是计算机的并行处理。
异步并行
每件事情使用异步处理的方法,但是又同时做多件事情,这就是异步并行。比如前面提到的网络传输、文件读写、数据库存取等操作,既用异步来提高工作效率,也同时做多件来提高效率,这就是异步并行,尤其是在Web服务应用方面,异步并行显著提高系统的吞吐量和响应性!
异步并行,并发 + 并行: Web + 机器学习¶
使用 FastAPI,可以利用 Web 开发中常见的并发机制的优势(NodeJS 的主要吸引力)。
并且,您也可以利用并行和多进程(让多个进程并行运行)的优点来处理与机器学习系统中类似的 CPU 密集型 工作。
这一点,再加上 Python 是“数据科学”、机器学习(尤其是深度学习)的主要语言这一简单事实,使得 FastAPI 与数据科学/机器学习 Web API 和应用程序(以及其他许多应用程序)非常匹配。
了解如何在生产环境中实现这种并行性,可查看此文 Deployment。
async
和 await
¶
现代版本的 Python 有一种非常直观的方式来定义异步代码。这使它看起来就像正常的"顺序"代码,并在适当的时候"等待"。
当有一个操作需要等待才能给出结果,且支持这个新的 Python 特性时,您可以编写如下代码:
burgers = await get_burgers(2)
这里的关键是 await
。它告诉 Python 它必须等待 ⏸ get_burgers(2)
完成它的工作 🕙 ,然后将结果存储在 burgers
中。这样,Python 就会知道此时它可以去做其他事情 🔀 ⏯ (比如接收另一个请求)。
要使 await
工作,它必须位于支持这种异步机制的函数内。因此,只需使用 async def
声明它:
async def get_burgers(number: int):# Do some asynchronous stuff to create the burgersreturn burgers
...而不是 def
:
# This is not asynchronous
def get_sequential_burgers(number: int):# Do some sequential stuff to create the burgersreturn burgers
使用 async def
,Python 就知道在该函数中,它将遇上 await
,并且它可以"暂停" ⏸ 执行该函数,直至执行其他操作 🔀 后回来。
当你想调用一个 async def
函数时,你必须"等待"它。因此,这不会起作用:
# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)
因此,如果您使用的库告诉您可以使用 await
调用它,则需要使用 async def
创建路径操作函数 ,如:
@app.get('/burgers')
async def read_burgers():burgers = await get_burgers(2)return burgers
官方的例子需要在fastapi框架内使用才行,这里写了个简单的帮助记忆:
>>> async def read_burgers():
... burgers = await get_burgers(2)
... return burgers
...
>>> def get_burgers(number: int):
... return number*1234567
...
>>> y = read_burgers()
>>> print(y)
<coroutine object read_burgers at 0x8be937f4110>
单独异步示例(不包含fastapi代码)
async
和 await的异步函数
import asyncio async def say_hello(): print("Hello!") await asyncio.sleep(1) # 模拟异步操作,比如 I/O 操作 print("World!") # 运行异步函数
asyncio.run(say_hello())
运行结果就是,输出Hello,然后等一秒,再输出world 。在等一秒的时间内,可以做其它异步的事情。
异步并行
import asyncio async def task(name, duration): print(f"Task {name} started") await asyncio.sleep(duration) print(f"Task {name} sleep {duration} sec completed") async def main(): # 创建多个异步任务 tasks = [ task("A", 2), task("B", 1), task("C", 3) ] # 并发执行这些任务 await asyncio.gather(*tasks) # 运行主异步函数
asyncio.run(main())
输出:
>>> asyncio.run(main())
Task A started
Task B started
Task C started
Task B sleep 1 sec completed
Task A sleep 2 sec completed
Task C sleep 3 sec completed
异步上下文管理器
import asyncio class MyAsyncContext: async def __aenter__(self): print("Entering context") return self async def __aexit__(self, exc_type, exc_val, exc_tb): print("Exiting context") async def main(): async with MyAsyncContext() as ctx: print("Inside context") await asyncio.sleep(1) # 运行主异步函数
asyncio.run(main())
在这个例子中,MyAsyncContext
是一个自定义的异步上下文管理器,它会在进入和退出上下文时执行异步操作。
输出:
asyncio.run(main())
Entering context
Inside context
Exiting context
异步 I/O 操作
异步编程最常见的应用场景之一是处理 I/O 操作,如网络请求。aiohttp
是一个流行的异步 HTTP 客户端库。
示例:使用 aiohttp
发起异步 HTTP 请求
import aiohttp
import asyncio async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: html = await fetch(session, 'http://example.com') print(html[:100]) # 打印响应的前 100 个字符 # 运行主异步函数
asyncio.run(main())
输出前100个字符:
asyncio.run(main())
<!doctype html>
<html>
<head><title>Example Domain</title><meta charset="utf-8" /><m
其它:更多技术细节¶
您可能已经注意到,await
只能在 async def
定义的函数内部使用。
但与此同时,必须"等待"通过 async def
定义的函数。因此,带 async def
的函数也只能在 async def
定义的函数内部调用。
那么,这关于先有鸡还是先有蛋的问题,如何调用第一个 async
函数?
如果您使用 FastAPI,你不必担心这一点,因为"第一个"函数将是你的路径操作函数,FastAPI 将知道如何做正确的事情。
但如果您想在没有 FastAPI 的情况下使用 async
/ await
,则可以这样做。
编写自己的异步代码¶
Starlette (和 FastAPI) 是基于 AnyIO 实现的,这使得它们可以兼容 Python 的标准库 asyncio 和 Trio。
特别是,你可以直接使用 AnyIO 来处理高级的并发用例,这些用例需要在自己的代码中使用更高级的模式。
即使您没有使用 FastAPI,您也可以使用 AnyIO 编写自己的异步程序,使其拥有较高的兼容性并获得一些好处(例如, 结构化并发)。
其他形式的异步代码¶
这种使用 async
和 await
的风格在语言中相对较新。
但它使处理异步代码变得容易很多。
这种相同的语法(或几乎相同)最近也包含在现代版本的 JavaScript 中(在浏览器和 NodeJS 中)。
但在此之前,处理异步代码非常复杂和困难。
在以前版本的 Python,你可以使用多线程或者 Gevent。但代码的理解、调试和思考都要复杂许多。
在以前版本的 NodeJS / 浏览器 JavaScript 中,你会使用"回调",因此也可能导致回调地狱。
协程¶
"协程"只是 async def
函数返回的一个非常奇特的东西的称呼。Python 知道它有点像一个函数,它可以启动,也会在某个时刻结束,而且它可能会在内部暂停 ⏸ ,只要内部有一个 await
。
通过使用 async
和 await
的异步代码的所有功能大多数被概括为"协程"。它可以与 Go 的主要关键特性 "Goroutines" 相媲美。
总结¶
让我们再来回顾下上文所说的:
Python 的现代版本可以通过使用
async
和await
语法创建“协程”,并用于支持“异步代码”。
现在应该能明白其含义了。✨
所有这些使得 FastAPI(通过 Starlette)如此强大,也是它拥有如此令人印象深刻的性能的原因。