在 FastAPI 中使用 `async def` 与 `def` 并测试阻塞调用

Using `async def` vs `def` in FastAPI and testing blocking calls

tl;dr

  1. 以下哪个选项是 fastapi 中的正确工作流程?
  2. 如何以编程方式测试调用是否真正阻塞(除了从浏览器手动测试)? uvicornfastapi 是否有 压力测试 扩展?

我在 fastapi 服务器(目前使用 uvicorn)中有许多端点,它们对常规同步 Python 代码的调用很长。尽管有文档 (https://fastapi.tiangolo.com/async/),我仍然不清楚我是否应该专门使用 defasync 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 - 根据基础调用混合搭配 defasync 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,它可以用作从端点创建任务的依赖项。

在我写这篇文章时,这似乎很固执己见,这可能是您直到现在才收到很多反馈的原因。因此,如果您不同意任何内容,请随时将我击倒:-)