根据FastAPI's documentation:
当你用普通的def声明一个路径操作函数时
async def,它在外部线程池中运行,然后
等待,而不是直接调用(因为它会阻塞
服务器)。
因此,def(同步)路由在与线程池不同的线程中运行,或者换句话说,服务器同时处理请求,而async def 路由在主线程上运行(单)线程,即服务器按顺序处理请求 - 只要在此类路由中没有await 调用I/O-bound 操作,例如等待 来自客户端的要通过网络发送的数据、要读取的磁盘中文件的内容、要完成的数据库操作等 - 看看here。带有async and await is many times summarised as using coroutines 的异步代码。 协程是协作的(或cooperatively multitasked):“在任何给定时间,具有协程的程序只运行它的一个协程,并且这个正在运行的协程只有在它明确请求暂停时才会暂停其执行"(有关协程的更多信息,请参阅 here 和 here)。但是,这不适用于CPU-bound 操作,例如here 描述的操作(例如,音频或图像处理、机器学习)。 CPU-bound 操作,即使在 async def 函数中声明并使用 await 调用,也会阻塞主线程。这也意味着 async def 路由中的阻塞操作(例如 time.sleep())将阻塞整个服务器(如您的情况)。
因此,如果您的函数不会进行任何async 调用,您可以改为使用def 声明它,如下所示:
@app.get("/ping")
def ping(request: Request):
#print(request.client)
print("Hello")
time.sleep(5)
print("bye")
return "pong"
否则,如果你要调用async 函数,你必须要await,你应该使用async def。为了证明这一点,下面使用来自asyncio 库的asyncio.sleep() 函数。 here 和 here 也给出了类似的例子。
import asyncio
@app.get("/ping")
async def ping(request: Request):
print("Hello")
await asyncio.sleep(5)
print("bye")
return "pong"
如果两个请求大约同时到达,上述两个函数都将打印预期的输出 - 如您的问题中所述。
Hello
Hello
bye
bye
注意:当您第二次(第三次等)调用端点时,请记住从与浏览器主会话隔离的选项卡中执行此操作;否则,请求将显示为来自同一个客户端(您可以检查使用 print(request.client) - 如果两个选项卡在同一窗口中打开,port 数字将显示相同),因此,请求将按顺序处理。您可以重新加载相同的选项卡(正在运行),或者在隐身窗口中打开一个新选项卡,或者使用其他浏览器/客户端发送请求。
异步/等待和昂贵的 CPU 密集型操作(长计算任务)
如果您需要使用async def(因为您可能需要await 用于路由内的协程),但也有一些可能阻塞服务器并且不允许其他请求的同步长计算任务通过,例如:
@app.post("/ping")
async def ping(file: UploadFile = File(...)):
print("Hello")
try:
contents = await file.read()
res = some_long_computation_task(contents) # this blocks other requests
finally:
await file.close()
print("bye")
return "pong"
然后:
-
使用更多workers(例如uvicorn main:app --workers 4)。 注意:每个工人"has its own things, variables and memory"。这意味着global 变量/对象等不会在进程/工作者之间共享。在这种情况下,您应该考虑使用数据库存储或键值存储(缓存),如here 和here 所述。此外,“如果您在代码中消耗大量内存,每个进程将消耗等量的内存”。
-
使用来自concurrency 模块(源代码here 和here)的FastAPI(Starlette's)run_in_threadpool() - 正如@tiangolo 建议的here - “将在单独的线程中运行函数以确保主线程(运行协程的地方)不会被阻塞”(参见here)。正如@tiangolo here 所描述的,“run_in_threadpool 是一个可等待函数,第一个参数是一个普通函数,下一个参数直接传递给该函数。它支持序列参数和关键字参数”。
from fastapi.concurrency import run_in_threadpool
res = await run_in_threadpool(some_long_computation_task, contents)
-
或者,使用asyncio的run_in_executor:
loop = asyncio.get_running_loop()
res = await loop.run_in_executor(None, lambda: some_long_computation_task(contents))
-
您还应该检查是否可以将路由定义更改为def。例如,如果您的端点中唯一需要等待的方法是读取文件内容的方法(正如您在下面的 cmets 部分中提到的),FastAPI 可以为您读取文件的bytes(但是,这应该适用于小文件,因为整个内容将存储在内存中,请参阅here),或者您甚至可以直接调用SpooledTemporaryFile对象的read()方法,这样您就不必等待@ 987654388@ 方法 - 由于您现在可以使用 def 声明您的路由,因此每个请求都将在单独的线程中运行。
@app.post("/ping")
def ping(file: UploadFile = File(...)):
print("Hello")
try:
contents = file.file.read()
res = some_long_computation_task(contents)
finally:
file.file.close()
print("bye")
return "pong"
-
查看this answer,以及文档here,了解更多建议的解决方案。