在 FastAPI 中使用 `async def` 与 `def` 并测试阻塞调用
Using `async def` vs `def` in FastAPI and testing blocking calls
tl;dr
- 以下哪个选项是
fastapi
中的正确工作流程?
- 如何以编程方式测试调用是否真正阻塞(除了从浏览器手动测试)?
uvicorn
或 fastapi
是否有 压力测试 扩展?
我在 fastapi
服务器(目前使用 uvicorn
)中有许多端点,它们对常规同步 Python 代码的调用很长。尽管有文档 (https://fastapi.tiangolo.com/async/),我仍然不清楚我是否应该专门使用 def
、async def
或混合使用我的函数。
据我了解,我有三种选择,假设:
def some_long_running_sync_function():
...
选项 1 - 始终只对端点使用 def
@app.get("route/to/endpoint")
def endpoint_1:
some_long_running_sync_function()
@app.post("route/to/another/endpoint")
def endpoint_2:
...
选项 2 - 始终只使用 async def
并且 运行 阻塞执行器中的同步代码
import asyncio
@app.get("route/to/endpoint")
async def endpoint_1:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, some_long_running_sync_function)
@app.post("route/to/another/endpoint")
async def endpoint_2:
...
选项 3 - 根据基础调用混合搭配 def
和 async def
import asyncio
@app.get("route/to/endpoint")
def endpoint_1:
# endpoint is calling to sync code that cannot be awaited
some_long_running_sync_function()
@app.post("route/to/another/endpoint")
async def endpoint_2:
# this code can be awaited so I can use async
...
因为没有人选择这个,所以我给了我的两分钱。所以这仅源于我的个人经验:
选项 1
这完全违背了使用异步框架的目的。所以,可能,但没有用。
选项 2 和 选项 3
对我来说,这是两者的结合。该框架明确地支持同步和异步端点的混合。通常我会选择: function/CPU-bound 任务中没有 await
-> def
,IO-bound/very short -> async
.
陷阱
当您开始使用异步编程时,很容易误入歧途,认为现在您不必再关心线程安全了。但是一旦你在线程池中开始 运行 东西,这就不再是真的了。所以请记住,FastAPI 是 运行 您在线程池中的 def
端点,您有责任使它们成为线程安全的。
这也意味着您需要考虑在 def
端点中事件循环可以做什么和不能做什么。由于您的事件循环在主线程中是 运行,因此 asyncio.get_running_loop()
将不起作用。这就是为什么我有时会定义 async def
个端点,即使没有 IO,也能从同一个线程访问事件循环。但是当然你必须保持你的代码简短。
附带说明:FastAPI 还公开了 starlettes backgroundtasks
,它可以用作从端点创建任务的依赖项。
在我写这篇文章时,这似乎很固执己见,这可能是您直到现在才收到很多反馈的原因。因此,如果您不同意任何内容,请随时将我击倒:-)
tl;dr
- 以下哪个选项是
fastapi
中的正确工作流程? - 如何以编程方式测试调用是否真正阻塞(除了从浏览器手动测试)?
uvicorn
或fastapi
是否有 压力测试 扩展?
我在 fastapi
服务器(目前使用 uvicorn
)中有许多端点,它们对常规同步 Python 代码的调用很长。尽管有文档 (https://fastapi.tiangolo.com/async/),我仍然不清楚我是否应该专门使用 def
、async def
或混合使用我的函数。
据我了解,我有三种选择,假设:
def some_long_running_sync_function():
...
选项 1 - 始终只对端点使用 def
@app.get("route/to/endpoint")
def endpoint_1:
some_long_running_sync_function()
@app.post("route/to/another/endpoint")
def endpoint_2:
...
选项 2 - 始终只使用 async def
并且 运行 阻塞执行器中的同步代码
import asyncio
@app.get("route/to/endpoint")
async def endpoint_1:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, some_long_running_sync_function)
@app.post("route/to/another/endpoint")
async def endpoint_2:
...
选项 3 - 根据基础调用混合搭配 def
和 async def
import asyncio
@app.get("route/to/endpoint")
def endpoint_1:
# endpoint is calling to sync code that cannot be awaited
some_long_running_sync_function()
@app.post("route/to/another/endpoint")
async def endpoint_2:
# this code can be awaited so I can use async
...
因为没有人选择这个,所以我给了我的两分钱。所以这仅源于我的个人经验:
选项 1
这完全违背了使用异步框架的目的。所以,可能,但没有用。
选项 2 和 选项 3
对我来说,这是两者的结合。该框架明确地支持同步和异步端点的混合。通常我会选择: function/CPU-bound 任务中没有 await
-> def
,IO-bound/very short -> async
.
陷阱
当您开始使用异步编程时,很容易误入歧途,认为现在您不必再关心线程安全了。但是一旦你在线程池中开始 运行 东西,这就不再是真的了。所以请记住,FastAPI 是 运行 您在线程池中的 def
端点,您有责任使它们成为线程安全的。
这也意味着您需要考虑在 def
端点中事件循环可以做什么和不能做什么。由于您的事件循环在主线程中是 运行,因此 asyncio.get_running_loop()
将不起作用。这就是为什么我有时会定义 async def
个端点,即使没有 IO,也能从同一个线程访问事件循环。但是当然你必须保持你的代码简短。
附带说明:FastAPI 还公开了 starlettes backgroundtasks
,它可以用作从端点创建任务的依赖项。
在我写这篇文章时,这似乎很固执己见,这可能是您直到现在才收到很多反馈的原因。因此,如果您不同意任何内容,请随时将我击倒:-)