我与此项目毫无关系。
接口假设一个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(): # 分解因数
...
这会完全阻塞工作进程,直到它运行结束。这里有些需要注意的地方。
- 工作线程完全被阻塞——因此,例如访问/health将会超时。
- 大多数阻塞任务不像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 # 返回计算结果
那么,到底在搞什么
- 在您 FastAPI 应用中的每个 Uvicorn 工作者内部都有一个事件循环在运行。
- 通过将 factorize 任务提交到
loop.run_in_executor
,它将在一个新线程中运行。这些线程池中的线程由线程池管理,因此不会无限制地增加。 - 简而言之;factorize 端点将不再阻塞住传入的
/health
调用,就像同步版本所做的那样。
/health
接口保持响应性,因为这些请求在主事件循环里处理,而那些繁重的任务则在独立的线程中运行。
我为什么还要在ChatGPT什么都会做得更好得多的情况下写博客呢?
当这种情况发生时,如果许多对 /factorize
的请求同时到达,它们会变得越来越慢,因为您的操作系统会在这些线程间频繁切换上下文,因此主事件循环仍然能够迅速响应 /health
和其他端点,以保证系统的整体稳定性。
ChatGPT建议最好继续使用async
,为了代码的一致性,即使这些端点很简单等等。
TLDR;即使是简单的端点,如
/health
,也建议定义为异步的 (async def
)。保持所有端点的异步性确保了代码库的一致性,这简化了维护并减少了混用同步和异步代码可能出现的错误。此外,如果你将来想在健康检查中添加异步操作,比如检查数据库连接或外部服务的状态,你无需重构该端点。在整个应用程序中使用async
有助于提高可扩展性,并为未来的功能增强保留了灵活性。
- 在所有端点定义中使用
async
(或者至少在所有复杂的端点定义中使用)。 - 在执行非阻塞操作,如 HTTP 请求和数据库查询时,使用
await
— 确保使用像httpx
和asyncpg
这样的异步兼容库。 - 对于阻塞或 CPU 密集型任务使用
run_in_executor
以防止工作线程无响应(例如,导致健康检查失败)。 - 对于
/health
和其他快速执行的非简单端点,不要使用await
或run_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 {"状态": "健康"}
共同学习,写下你的评论
评论加载中...
作者其他优质文章