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

Posted

技术标签:

【中文标题】在 FastAPI 中使用 `async def` 与 `def` 并测试阻塞调用【英文标题】:Using `async def` vs `def` in FastAPI and testing blocking calls 【发布时间】:2021-11-26 11:43:49 【问题描述】:

tl;dr

    以下哪个选项是fastapi 中正确的工作流程? 如何以编程方式测试呼叫是否真正阻塞(除了从浏览器手动)? 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】:

由于没有人捡起这个,我给了我的两分钱。所以这仅源于我的个人经验:

选项 1

这完全违背了使用异步框架的目的。所以,可能,但没用。

选项 2选项 3

对我来说,两者兼而有之。该框架明确地支持同步和异步端点的混合。通常我会选择:没有await in the function/CPU-bound task -> def, IO-bound/very short -> async.

陷阱

当您开始使用异步编程时,很容易陷入现在您不必再关心线程安全的谬论。但是一旦你开始在线程池中运行东西,这就不是真的了。所以请记住,FastAPI 在线程池中运行您的 def 端点,您有责任使它们成为线程安全的。

这也意味着您需要考虑在def 端点中您可以使用事件循环做什么和不可以做什么。由于您的事件循环在主线程中运行,asyncio.get_running_loop() 将不起作用。这就是为什么我有时会定义async def 端点,即使没有 IO,也能够从同一个线程访问事件循环。但是当然你必须保持你的代码简短。

附带说明:FastAPI 还公开了 starlettes backgroundtasks,它可以用作从端点创建任务的依赖项。

在我写这篇文章的时候,这似乎很固执,这可能是你直到现在还没有得到很多反馈的原因。所以,如果你不同意任何事情,请随意击落我:-)

【讨论】:

感谢@thisisalsomypassword 的回答 - 我完全同意选项 1 违背了fastapi 的目的。现在,选项 2-3 是整个端点在单独的执行程序中运行(选项 3)还是只有长时间运行的任务在单独声明的执行程序中运行(选项 2)的唯一区别——本质上是在同步并将其视为异步(来自执行者的await)? 是的,我想基本上就是这样。正如您所说,执行人是“单独宣布的”。因此,您的工作如何发送到线程的细节当然会有所不同。如果在“选项 2”中的 endpoint_1 中没有其他 IO 绑定,我不会将其声明为 async def

以上是关于在 FastAPI 中使用 `async def` 与 `def` 并测试阻塞调用的主要内容,如果未能解决你的问题,请参考以下文章

@asyncio.coroutine 与 async def

测试从常规函数调用 python 协程(async def)

测试从常规函数调用 python 协程(async def)

async def on_ready(): SyntaxError: 无效语法

调用`async for in`后如何获得异步生成器的下一次迭代

何时/何地在 FastAPI 中使用正文/路径/查询/字段?