为了账号安全,请及时绑定邮箱和手机立即绑定

最简单的:何时在 FastAPI 中使用异步

标签:
Python API
这斗争确实存在

我与此项目毫无关系。

接口

假设一个FastAPI(快速API框架)应用,它有三个接口。

  • /调用其他服务
  • /因子分解
  • /健康

这由三种方法支持

    # 非阻塞IO
    def call_other_service():

    # 阻塞且占用大量CPU
    def factorize():

    # 简单 - 立即返回结果
    def health():

好的,那我是在这些地方都加上 async 呢?除非,你的应用就是个玩具或者复杂度很低,或者依赖那些不支持 await 的库,否则我建议一开始就用上 async

async def 调用其他服务()  
async def 因子分解()  
async def 健康检查()  

当然——健康检查非常简单,甚至可以省去异步处理,但在混合搭配时可能会变得棘手。

调用其他服务:

这个端点表示一个非阻塞IO操作,这里指的是一个后台操作,就像你点了一个咖啡,咖啡师开始做咖啡,你可以继续做其他事情,直到咖啡做好了为止。所以我们最好用await。这到底是什么东西?它指的是调用其他东西并等待情况。例如:

  • 发起一个 HTTP 请求
  • 异步数据库查询操作

在进程等待这些结果返回时,CPU没有被充分利用,所以我们希望利用 await 来解决这个问题——这里我们可以使用 httpx 库:

    import httpx  
    from fastapi import FastAPI  

    app = FastAPI()  

    # 从外部服务获取数据
    async def _fetch():  
        async with httpx.AsyncClient() as client:  
            response = await client.get("https://jsonplaceholder.typicode.com/todos/1")  
            return response.json()  # 非阻塞的异步请求  

    # FastAPI 端点接口,调用异步函数
    @app.get("/call_other_service")  
    async def call_other_service():  
        data = await _fetch()  # 调用异步函数  
        return data

这类似于数据库查询,但我们需要使用一个异步驱动以便利用非阻塞I/O。

    import asyncpg  
    from fastapi import FastAPI  

    app = FastAPI()  

    # 一个异步函数,用于从数据库获取数据  
    async def _fetch_db_data():  
        conn = await asyncpg.connect(user='user', password='password', database='dbname', host='localhost')  
        rows = await conn.fetch("SELECT * FROM your_table WHERE some_column = 'some_value'")  # 非阻塞的数据库查询  
        await conn.close()  
        return rows  

    # FastAPI端点,用于调用异步函数获取数据  
    @app.get("/call_other_service")  
    async def call_other_service():  
        data = await _fetch_db_data()  # 调用异步函数获取数据  
        return {"data": data}
/分解因式

与非阻塞 IO 操作不同,CPU 在这种情况下只是空闲,我们则有 CPU 密集型操作或阻塞操作,这些操作包括:

  • 进行因式分解
  • 使用 subprocess 库来
  • 打印一万亿次的 'hello world'

所以你可能会天真地以为,只需将阻塞任务设置为同步,例如:

@app.get("/factorize")  
def factorize():  # 分解因数
    ...

这会完全阻塞工作进程,直到它运行结束。这里有些需要注意的地方。

  1. 工作线程完全被阻塞——因此,例如访问/health将会超时。
  2. 大多数阻塞任务不像factorize那样完全占用CPU,所以在某些时候,进程会浪费计算周期,而同步执行时这些周期却被白白浪费了。

我们可以利用 asyncio(异步IO)循环

    导入 asyncio 模块

    # 执行 CPU 密集型计算的同步函数  
    def _factorize():  
        # CPU 密集型计算  
        ...  

    @app.get("/factorize")  
    异步定义 factorize 函数:  
        loop = asyncio.get_running_loop()  # 获取正在运行的事件循环  
        result = await loop.run_in_executor(None, _factorize)  # 在单独的线程中执行 _factorize 函数  
        return result  # 返回计算结果

那么,到底在搞什么

  1. 在您 FastAPI 应用中的每个 Uvicorn 工作者内部都有一个事件循环在运行。
  2. 通过将 factorize 任务提交到 loop.run_in_executor,它将在一个新线程中运行。这些线程池中的线程由线程池管理,因此不会无限制地增加。
  3. 简而言之;factorize 端点将不再阻塞住传入的 /health 调用,就像同步版本所做的那样。

/health接口保持响应性,因为这些请求在主事件循环里处理,而那些繁重的任务则在独立的线程中运行。

我为什么还要在ChatGPT什么都会做得更好得多的情况下写博客呢?

当这种情况发生时,如果许多对 /factorize 的请求同时到达,它们会变得越来越慢,因为您的操作系统会在这些线程间频繁切换上下文,因此主事件循环仍然能够迅速响应 /health 和其他端点,以保证系统的整体稳定性。

/健康

ChatGPT建议最好继续使用async,为了代码的一致性,即使这些端点很简单等等。

即使是简单的端点,如 /health,也建议定义为异步的 (async def)。保持所有端点的异步性确保了代码库的一致性,这简化了维护并减少了混用同步和异步代码可能出现的错误。此外,如果你将来想在健康检查中添加异步操作,比如检查数据库连接或外部服务的状态,你无需重构该端点。在整个应用程序中使用 async 有助于提高可扩展性,并为未来的功能增强保留了灵活性。

TLDR;
  1. 在所有端点定义中使用 async (或者至少在所有复杂的端点定义中使用)。
  2. 在执行非阻塞操作,如 HTTP 请求和数据库查询时,使用 await — 确保使用像 httpxasyncpg 这样的异步兼容库。
  3. 对于阻塞或 CPU 密集型任务使用 run_in_executor 以防止工作线程无响应(例如,导致健康检查失败)。
  4. 对于 /health 和其他快速执行的非简单端点,不要使用 awaitrun_in_executor
完整的例子
    import asyncio  
    import asyncpg  
    from fastapi import FastAPI  

    app = FastAPI()  

    # 异步函数,从数据库中获取数据  
    async def _fetch_db_data():  
        conn = await asyncpg.connect(  
            user='user',  
            password='password',  
            database='dbname',  
            host='localhost'  
        )  
        rows = await conn.fetch("SELECT * FROM your_table WHERE some_column = 'some_value'")  
        await conn.close()  
        return rows  

    # FastAPI 端点,用于调用内部函数执行数据库查询  
    @app.get("/call_other_service")  
    async def call_other_service():  
        data = await _fetch_db_data()  
        return {"data": data}  

    # 进行 CPU 密集型计算的同步函数  
    def _factorize():  
        # CPU 密集型计算  
        ...  

    @app.get("/factorize")  
    async def factorize():  
        loop = asyncio.get_running_loop()  
        result = await loop.run_in_executor(None, _factorize)  
        return result  

    @app.get("/health")  
    async def health_check():  
        return {"状态": "健康"}
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消