FastAPI中如果async def和def 路由的区别

在python的整体生态中,虽然已经有很多库支持了异步调用,如可以使用httpx或者aiohttp代替requests库发起http请求,使用asyncio.sleep 代替time.sleep, 但是依然还有很多优秀的第三方库是不支持异步调用也没有可代替的库,那么如何在FastAPI中调用这种没有实现异步的库但是又不阻塞整个系统呢?

查看官方文档,https://fastapi.tiangolo.com/zh/async/ ,最后有个简要的结论

  1. 如果你的函数中有使用await,则函数使用async def 定义。
  2. 如果不知道是否要使用async,则不要用,使用def 就好。

原理:

**在python 中,使用async def 定义的函数是运行在协程中,而多个协程是在一个主线程中的。fastapi中的async def协程路由处理的请求会全部放在main thread,而def 处理的请求每个都有自己的独立线程,fastapi内部会自动为这两种接口进行对应的处理逻辑 **

我们来详细看一下各种情况。本文将使用await asyncio.sleep(5) 来模拟实现了异步操作的库。time.sleep(5) 来模拟只有同步操作的库。

只有同步库

import time  from fastapi import FastAPI, Request  
import asyncio  
import threading  
import uvicorn  app = FastAPI()  @app.middleware("http")  
async def cal_time(req: Request, call_next):  start = time.time()  response = await call_next(req)  process_time = time.time() - start  print(f"接口{req.url.path} use {process_time} sec.")  return response  @app.get("/test1")  
def test1():  print(threading.current_thread(), "test1")  return "test1"  @app.get("/test2")  
def test2():  print(threading.current_thread(), "test2")  time.sleep(5)  return "test2"  if __name__ == '__main__':  uvicorn.run(app, host="127.0.0.1", port=8000)

上面有的fastapi 定义了两个接口,test1接口未执行任何IO操作,只返回一个字符串,用来验证其它接口是否会阻塞test1接口,test2 接口,这里使用time.sleep(5) 来模拟一个阻塞操作。并且同时打印了test1和test2中的线程信息。并且添加了一个中间件cal_time,打印每个请求的耗时。

我们启动应用并看一下此时存在阻塞的接口test2是否会阻塞test1接口?

我们在浏览器中先访问test2接口,之后再请求test1接口,我们来看一下打印

<WorkerThread(AnyIO worker thread, started 11720)> test2
<WorkerThread(AnyIO worker thread, started 22508)> test1
接口/test1 use 0.0019936561584472656 sec.
INFO:     127.0.0.1:56758 - "GET /test1 HTTP/1.1" 200 OK
接口/test2 use 5.0042359828948975 sec.
INFO:     127.0.0.1:56757 - "GET /test2 HTTP/1.1" 200 OK
  1. 由于先请求的test2接口,所以先打印出了test2接口的线程信息,在11720 线程上执行。
  2. 之后请求了test1接口,这时打印了test1接口所运行的线程,线程号为22508。
  3. 此时 test1 接口并没有因为test2接口的time.sleep(5) 而阻塞,而是直接返回了test1,显示耗时0.001秒。
  4. 在停了5秒钟以后,接口test2完成了响应,显示耗时5秒。

有异步库

在只有同步调用库的情况下,上面的代码运行很好,接口之间并没有相互阻塞,接下来我们看一下存在异步库的情况。

import time  from fastapi import FastAPI, Request  
import asyncio  
import threading  
import uvicorn  app = FastAPI()  @app.middleware("http")  
async def cal_time(req: Request, call_next):  start = time.time()  response = await call_next(req)  process_time = time.time() - start  print(f"接口{req.url.path} use {process_time} sec.")  return response  @app.get("/test1")  
def test1():  print(threading.current_thread(), "test1")  return "test1"  @app.get("/test2")  
async def test2():  print(threading.current_thread(), "test2")  await asyncio.sleep(5)  return "test2"  if __name__ == '__main__':  uvicorn.run(app, host="127.0.0.1", port=8000)

这里修改test2方法,使用 await asyncio.sleep(5) 模拟异步阻塞操作,由于这里使用了await, 所以需要将test2改为async def,这时还是先访问test2接口,再访问test1接口,此时test2中的异步IO操作并依然没有阻塞test1接口,我们再来看一下请求的打印输出。


  1. 首先访问test2接口,打印了test2接口的线程信息。
  2. 接下来访问test1接口,打印了test1接口的线程信息。
  3. 由于test2接口并不会阻塞test1接口,所以这里test1接口完成请求,耗时0.003秒。
  4. 过了5秒钟以后,接口2阻塞结束,打印耗时5秒。

两次接口test2都不会阻塞test1,这是为什么呢?以前在使用Tornado或者Sanic开发时,如果某个接口里调用了同步库,如requests.get,那么如果处理不当是会阻塞整个系统进程的。

我们来仔细看一下程序的打印输出,在第一次使用time.sleep(5) 模拟IO操作时,打印的线程信息为

<WorkerThread(AnyIO worker thread, started 11720)> test2
<WorkerThread(AnyIO worker thread, started 22508)> test1

我们看到两次请求是在不同的线程中运行的,所以即使某个接口请求中存在阻塞操作,也不会影响到其它的线程。

再看第二次使用await asyncio.sleep(5) 模拟IO操作时的线程信息。

<_MainThread(MainThread, started 4501536256)> test2
<WorkerThread(AnyIO worker thread, started 123145549803520)> test1

依然是在两个不同的线程中运行的,所以也不会相互阻塞。

但是我们在横向比较两次请求中的test2的线程信息

<WorkerThread(AnyIO worker thread, started 11720)> test2
<_MainThread(MainThread, started 4501536256)> test2

第一次在使用def 定义函数时,是在WorkerThread中运行的,第二次使用async def 显示是在MainThread 中运行的,这也就说明了一个问题,在python 中,使用async def 定义的函数是运行在协程中,而多个协程是在一个主线程中的。

容易出的问题

通过上面的实践,我们再来看最开始时,FastAPI官网给出的结论:

  1. 如果你的函数中有使用await,则函数使用async def 定义。
  2. 如果不知道是否要使用async,则不要用,使用def 就好。

这个结论看起来比较简单明了,但是我们也看出一些问题,如果存在await,则必须要定义在async def 函数中,这个好说,如果没有定义在async def 函数中,则语法都过不去。对于没有存在异步操作的库,我们应不应该定义在async中呢?我们来实践一下。修改test2函数。

import time  from fastapi import FastAPI, Request  
import asyncio  
import threading  
import uvicorn  
from uvicorn.config import LOGGING_CONFIG  app = FastAPI()  @app.middleware("http")  
async def cal_time(req: Request, call_next):  start = time.time()  response = await call_next(req)  process_time = time.time() - start  print(f"接口{req.url.path} use {process_time} sec.")  return response  @app.get("/test1")  
def test1():  print(threading.current_thread(), "test1")  return "test1"  @app.get("/test2")  
async def test2():  print(threading.current_thread(), "test2")  time.sleep(5)  return "test2"  if __name__ == '__main__':  LOGGING_CONFIG["formatters"]["access"][  "fmt"] = '%(asctime)s %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'  uvicorn.run(app, host="127.0.0.1", port=8000)

这次我们打印一下请求的具体时间,由于默认的uvicorn 日志没有打印具体的时间,所以这里通过LOGGING_CONFIG重新定义一下日志的格式。在test2函数这时我们使用time.sleep(5) 替换掉await asyncio.sleep(5), 这时依然使用async def 定义test2函数,再访问一下test2和test1接口。

此时我们看到的现象为test1接口被test2接口给阻塞住了,但是由上面的实验得出来的结论,使用def 定义的函数应该是单启一个线程执行,可是为什么还被test2给阻塞了呢?

我们详细看一下输出:

<_MainThread(MainThread, started 3768)> test2
接口/test2 use 5.008129358291626 sec.
2023-11-29 11:33:12,724 INFO:     127.0.0.1:64011 - "GET /test2 HTTP/1.1" 200 OK<WorkerThread(AnyIO worker thread, started 20056)> test1
接口/test1 use 0.006971836090087891 sec.
2023-11-29 11:33:12,730 INFO:     127.0.0.1:64012 - "GET /test1 HTTP/1.1" 200 OK

输出分为两部分,一个是访问test2的输出,一个是访问test1的输出。

  1. test2的输出,首先打印出运行test2函数的线程,由于test2是async def 定义的, 所以这个协程是跑在_MainThread中。
  2. 此时test2函数运行到了time.sleep(5) 处,阻塞了整个系统,使得整个系统无法再接收新来的请求。
  3. 这时新发来了一个test1接口的请求,这时由于test2中的time.sleep 阻塞了整个系统,所以这时test1请求也被阻塞了,并不是test1函数被阻塞了,而是现在都无法开始执行test1接口函数。
  4. 过了5秒钟,执行完time.sleep(5) 代码,打印输出 2023-11-29 11:33:12,724 INFO: 127.0.0.1:64011 - "GET /test2 HTTP/1.1" 200 OK, 系统接着开始处理test1接口请求。
  5. 由于 test1使用def 定义,所以这里跑在了一个新的子线程中,WorkerThread,且test1没有任何IO阻塞操作,所以此时很快的完成了响应。打印耗时0.006。

这也就解释了为什么先访问阻塞的test2接口,再访问非阻塞的test1接口,即使test1接口使用def 定义也会被卡住的情况。

通过上面的实验,我们可以得出,如果存在同步阻塞的IO操作,不要 放到async def 函数中。

test1函数没有阻塞的IO操作,使用async def 或者 def 定义都可以,看官方的文档应该是推荐放到def 中,但是由于def 定义的函数在运行的时候需要单启一个线程(从线程池中获取),会有一些额外的开销,如果访问量小的话应该没有多大影响。

结论

在FastAPI中,如果使用async def 定义的函数,里面的IO操作均要实现异步操作await,如果要使用同步的IO操作,需要使用def 定义函数。简单来讲用下图表示

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/463597.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pinctrl子系统中Pincontroller和client驱动程序的编写

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析inctrl子系统中Pincontroller构造过程驱动分析&#xff1a;imx_pinctrl_soc_info结构体Pinctrl子系统中c…

【C++动态规划】2435. 矩阵中和能被 K 整除的路径|1951

本文涉及知识点 C动态规划 LeetCode2435. 矩阵中和能被 K 整除的路径 给你一个下标从 0 开始的 m x n 整数矩阵 grid 和一个整数 k 。你从起点 (0, 0) 出发&#xff0c;每一步只能往 下 或者往 右 &#xff0c;你想要到达终点 (m - 1, n - 1) 。 请你返回路径和能被 k 整除的…

【QT】Qt对话框

个人主页~ Qt窗口属性~ Qt窗口 五、对话框2、Qt内置对话框&#xff08;1&#xff09;Message Box&#xff08;2&#xff09;QColorDialog&#xff08;3&#xff09;QFileDialog&#xff08;4&#xff09;QFontDialog&#xff08;5&#xff09;QInputDialog 五、对话框 2、Qt内…

视频推荐的算法(字节青训)

题目&#xff1a; 西瓜视频 正在开发一个新功能&#xff0c;旨在将访问量达到80百分位数以上的视频展示在首页的推荐列表中。实现一个程序&#xff0c;计算给定数据中的80百分位数。 例如&#xff1a;假设有一个包含从1到100的整数数组&#xff0c;80百分位数的值为80&#…

线程基础知识、jmm(Java内存模型)

目录 线程基础知识 并发与并行 进程和线程 线程优先级 创建线程的方式主要有三种 休眠 作出让步 join() 方法 线程协作注意什么 理解线程状态 选择合适的协作工具 共享资源的访问控制 避免竞争条件 创建线程几种方式 线程状态&#xff0c;状态之间切换 新建&…

2.数组越界访问如何调试HardFault错误

数组越界 在项目开发过程中&#xff0c;配置串口外设是一个常见的任务&#xff0c;但在实际操作中&#xff0c;我们可能会遇到一些预料之外的问题。例如&#xff0c;在调试过程中&#xff0c;我们发现单片机只接受了一次数据后便不再接收&#xff0c;这无疑是一个棘手的问题。…

0-ARM Linux驱动开发-字符设备

一、字符设备概述 Linux 系统中&#xff0c;设备被分为字符设备、块设备和网络设备等。字符设备以字节流的方式进行数据传输&#xff0c;数据的访问是按顺序的&#xff0c;一个字节一个字节地进行读取和写入操作&#xff0c;没有缓冲区。例如&#xff0c;终端&#xff08;/dev…

openGauss数据库-头歌实验1-4 数据库及表的创建

一、创建数据库 &#xff08;一&#xff09;任务描述 本关任务&#xff1a;创建指定数据库。 &#xff08;二&#xff09;相关知识 数据库其实就是可以存放大量数据的仓库&#xff0c;学习数据库我们就从创建一个数据库开始吧。 为了完成本关任务&#xff0c;你需要掌握&a…

深入浅出 Spring Boot 与 Shiro:构建安全认证与权限管理框架

一、Shiro框架概念 &#xff08;一&#xff09;Shiro框架概念 1.概念&#xff1a; Shiro是apache旗下一个开源安全框架&#xff0c;它对软件系统中的安全认证相关功能进行了封装&#xff0c;实现了用户身份认证&#xff0c;权限授权、加密、会话管理等功能&#xff0c;组成一…

重学SpringBoot3-整合 Elasticsearch 8.x (一)客户端方式

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 这里写目录标题 1. 为什么选择 Elasticsearch&#xff1f;2. Spring Boot 3 和 Elasticsearch 8.x 的集成概述2.1 准备工作2.2 添加依赖 3. Elasticsearch 客户端配置方式…

MyBaitsPlus 基本用法简单整理

MyBaitsPlus 基本用法整理 查询单表查询查询单条数据写法一&#xff1a;&#xff08;this.getOne&#xff09;写法二&#xff1a;&#xff08;XxxMapper.selectById&#xff09;写法三&#xff1a;&#xff08;this.getById&#xff09; 查询 list 集合&#xff08;this.list&a…

基于MATLAB的战术手势识别

手势识别的研究起步于20世纪末&#xff0c;由于计算机技术的发展&#xff0c;特别是近年来虚拟现实技术的发展&#xff0c;手势识别的研究也到达一个新的高度。熵分析法是韩国的李金石、李振恩等人通过从背景复杂的视频数据中分割出人的手势形状&#xff0c;然后计算手型的质心…

Mac 配置SourceTree集成云效

1、背景 工作使用的是自己的笔记本&#xff0c;一个是比较卡&#xff0c;在一个是敏感信息比较多还是使用公司的电脑&#xff0c;但是系统是Mac就很麻烦&#xff0c;在网上找了帖子记录一下 2、配置 打开终端 ssh-keygen -t rsa #一直回车就行 cd .ssh cat id_rsa.pub #查…

【快速上手】pyspark 集群环境下的搭建(Yarn模式)

目录 前言&#xff1a; 一、安装步骤 安装前准备 1.第一步&#xff1a;安装python 2.第二步&#xff1a;在bigdata01上安装spark 3.第三步&#xff1a;同步bigdata01中的spark到bigdata02和03上 二、启动 三、可打开yarn界面查看任务 前言&#xff1a; 上一篇介绍的是…

使用Python多线程抓取某图网数据并下载图片

前言 在互联网开发领域&#xff0c;数据抓取是一项非常实用的技术。通过数据抓取&#xff0c;我们可以从网页上获取所需的信息&#xff0c;并将其转化为结构化数据&#xff0c;以便进一步分析或使用。本文将介绍如何利用Python编写一个多线程程序来抓取网页上的图片数据&#…

《IMM交互式多模型滤波MATLAB实践》专栏目录,持续更新……

专栏链接&#xff1a;https://blog.csdn.net/callmeup/category_12816762.html 专栏介绍 关于IMM的例程 双模型EKF&#xff1a; 【逐行注释】基于CV/CT模型的IMM|MATLAB程序|源代码复制后即可运行&#xff0c;无需下载三模型EKF&#xff1a; 【matlab代码】3个模型的IMM例程&…

鸿蒙开发案例:指南针

【1】引言&#xff08;完整代码在最后面&#xff09; 在本文中&#xff0c;我们将介绍如何使用鸿蒙系统&#xff08;HarmonyOS&#xff09;开发一个简单的指南针应用。通过这个案例&#xff0c;你可以学习如何使用传感器服务、状态管理以及UI构建等基本技能。 【2】环境准备 …

人工智能的发展与未来:从Yann LeCun的观点谈起

引言 在当今的人工智能&#xff08;AI&#xff09;领域&#xff0c;AGI&#xff08;通用人工智能&#xff09;已成为热门话题。许多专家认为&#xff0c;随着技术的不断发展&#xff0c;AGI的实现只是时间问题。然而&#xff0c;Yann LeCun——图灵奖得主、Meta首席AI科学家&a…

【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介

文章目录 3.4 函数式依赖注入技术 Functional injection techniques3.5 模块化依赖注入技术 Modular injection techniques 写在前面 上一篇的最后部分对第三章后续内容做了一个概括性的梳理&#xff0c;并给出了断开依赖项的最简单的实现方案&#xff0c;函数参数值注入法。本…

电磁兼容(EMC):整改案例(六)Y电容过大导致雷击浪涌炸机

目录 1. 异常现象 2. 原因分析 3. 整改方案 4. 总结 1. 异常现象 某金属外壳带接地线的产品按GB/T 17626.5进行雷击浪涌测试&#xff0c;在L&#xff0c;N线对PE进行4kV浪涌电压测试时&#xff0c;出现炸机现象&#xff0c;AC-DC电源芯片损坏。而在L&#xff0c;N线间进行2…