本文参考FastAPI教程https://fastapi.tiangolo.com/zh/tutorial
并发async/await
有关路径操作函数的async def
语法以及异步代码、并发和并行的一些背景知识。
- 通过
async def
声明你的路径操作函数:
@app.get('/')
async def read_results():
results = await some_library()
return results
- 正常情况的
def
声明
@app.get('/')
def results():
results = some_library()
return results
注意:你可以根据需要在路径操作函数中混合使用def
和async def
,并使用最适合你的方式去定义每个函数。FastAPI将为他们做正确的事情。无论如何,在上述任何情况下,FastAPI仍将异步工作,速度也非常快。但是,通过遵循上述步骤,它将能够进行一些性能优化。
技术细节
Python的现代版本支持通过一种叫"协程"——使用async
和await
语法的东西来写”异步代码“。
让我们在下面的部分中逐一介绍:
- 异步代码
async
和await
- 协程
异步代码
异步代码仅仅意味着编程语言有办法告诉计算机/程序在代码中的某个点,它将不得不等待在某些地方完成一些事情。让我们假设一些事情被称为”慢文件“。
所以,在等待”慢文件“完成的这段时间,计算机可以做一些其他工作。
然后计算机/程序每次有机会都会回来,因为它又在等待,或者它完成了当前所有的工作。而且它将查看它等待的所有任务中是否有已经完成的,做它必须做的事情。
接下来,它完成第一个任务(比如是我们的”慢文件“)并继续与之相关的一切。
这个”等待其他事情“通常指的是一些相对较慢(与处理器和RAM存储器的速度相比)的I/O操作,比如说:
- 通过网络发送来自客户端的数据
- 客户端接收来自网络中的数据
- 磁盘中要由系统读取并提供给程序的文件的内容
- 程序提供给系统的要写入磁盘的内容
- 一个API的远程调用
- 一个数据库操作,直到完成
- 一个数据库查询,直到返回结果
- 等等
这个执行的时间大多是在等待I/O操作,因此它们被叫做”I/O密集型“操作。
它被称为”异步“的原因是因为计算机/程序不必与慢任务”同步“,去等待任务完成的确切时刻,而在此期间不做任何事情直到能够获取任务结果才继续工作。
相反,作为一个”异步“系统,一旦完成,任务就可以排队等待一段时间(几微秒),等待计算机程序完成它要做的任何事情,然后回来获取结果并继续处理它们。
对于”同步“,它们通常也使用”顺序“一词,因为计算机程序在切换到另一个任务之前是按顺序执行所有步骤,即使这些步骤涉及到等待。
并发和并行
上述异步代码的思想有时也被称为”并发“,它不同于”并行“。
- 大多数Web应用有很多等待场景,因此并发系统更有意义,这种异步机制正是NodeJS受到欢迎的原因(尽管NodeJS不是并行的),以及Go作为编程语言的优势所在。
- 在大多数执行时间是由实际工作(而不是等待)占用的,并且计算机中的工作是由CPU完成的,这种”CPU 密集型“操作的情况,并行是必要的,例如,音频或图像处理,计算机视觉,机器学习,深度学习,这些场景都需要复杂的数学处理。
并发+并行: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 burgers
return burgers
而不是def
:
# This is not asynchronous
def get_sequential_burgers(number: int):
# Do some sequential stuff to create the burgers
return 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
更多技术细节
您可能已经注意到,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 def
函数返回的一个非常奇特的东西的称呼。Python知道它有点像一个函数,它可以启动,也会在某个时刻结束,而且它可能会在内部暂停,只要内部有一个await
。
通过使用async
和await
的异步代码的所有功能大多数被概括为”协程“。它可以与Go的主要关键特性”Goroutines“相媲美。