fastapi 下怎么正确使用 async和await?

声明:异步“请求” 和 异步“方法调用” 的区别

问题
如果使用uvicorn单进程、单线程启动fastapi服务,其中代码里:

@app.get("/bb")
async def read_item():
    await long_running_task(4)
    result = await long_running_task(4)
    return {"result": result}

这时,/bb的单次请求时,执行await long_running_task(4)任务先等待4s,结束后再次执行long_running_task(4)又是4s等待,总共就是8s,也就是在方法体内的await是串行的。

那当多次请求/aa时:

@app.get("/aa")
async def read_item():
    result = await long_running_task()
    return {"result": result}

这时,fastapi 会把2个异步请求分别转化为task,而不是coroutine了,并把task提交到事件循环中,即每个请求都是1个独立的任务
事件循环会并发地处理这些任务,因此每个请求的 await long_running_task(4) 调用会并发执行。
因此,每个请求的响应时长是 4 秒,而不是顺序累加成 8 秒。

【关键点】

1、 方法体内多个await :在同一个方法体内,不管有多少个“await ” ,它们都共享同一个任务。
await是并发 or 串行取决于在await执行前,是否存在多个已启动的task。【具体详见: 异步编程下await的理解

2、多个请求并发处理:当多个请求并发时,每个请求会被包装成一个独立的任务,并提交到事件循环中。事件循环会并发地处理这些任务,因此每个请求的await 调用会并发执行。

1、同步、异步方法 + 同步阻塞

from fastapi import FastAPI, Query, Request, Response
import uvicorn
import time
import sys
import os
import asyncio

sys.path.append(os.getcwd())
app = FastAPI(title="bbb")
#***************************************1、同步阻塞操作... ***************************************
### def不用async修饰:同步方法+同步阻塞 ### 
@app.get("/aa")
def aa():
    print("aa")
    time.sleep(3)  # 同步阻塞3s
    print("aaa")
    return "aaaaaa"


### 异步方法+同步阻塞 ### 
@app.get("/bb")
async def bb(): 
    ''''
    虽然bb用async关键字修饰成了一个异步函数(协程),但该方法内部使用了同步阻塞,所以导致它的异步能力失效,变成同步方法!!
    '''
    print("bb")
    time.sleep(8)  # 是一个同步阻塞操作。它会阻塞当前线程,直到指定的秒数过去。
    print("bbb")
    return "bbbbbb"

#***************************************2、非阻塞操作... ***************************************
### 异步方法 + 无阻塞 ### 
@app.get("/hello")
async def hello(con: str = Query(..., description="输入字符串")):
    print("hello")
    con = con + "**** 好样的!"
    return {"message": con}

### 同步方法 + 无阻塞 ### 
@app.get("/hello2")
def hello2(con: str = Query(..., description="输入字符串")):
    con = con + "**** 好样的222222!"
    return {"message": con}



if __name__ == "__main__":
    uvicorn.run(app,
                host="0.0.0.0",
                port=9351)  

1.1 仅同步请求的并发

fastapi 在处理同步请求时,会把他们放入底层维护的工作线程池中,每个请求由AnyIO worker thread(工作线程)执行。

测试

/aa 和 /hello2 并发:
/aa耗时3.02s, /hello2耗时5ms。

结论:/aa 的阻塞不会影响 /hello2 的及时返回。

1.2 仅异步请求的并发

fastapi 在处理异步请求时,会把他们放入Main Thread的协程event_loop中。如果一个异步方法里存在同步阻塞,那他会导致后请求的异步方法也被阻塞!
因为所有的异步都在1个event_loop中~~

测试

/bb 和 /hello 并发:
/hello要等/bb执行完才会结束,总体耗时8s。

结论:/bb 的阻塞会影响 /hello 的及时返回。

1.3 同步请求 和 异步请求 的并发

(1)/aa稍早于/bb

通过调用堆栈可以看到:
/aa即使阻塞也不会阻塞/bb的请求,两者可同步执行方法体!

但是,不知道为什么有时候/aa要等到/bb响应结束也才return, 也就是/aa的总响应时长不总是3s附近,会出现return bbbbbb结束后才收到return aaaaaa,导致/aa的响应时长大于8s!
执行结果:
aa
bb
aaa
bbb

(2)/bb稍早于/aa

通过调用堆栈可以看到:
/bb阻塞会导致/aa不能立即执行,而是要等待/bb完全执行完才会进入/aa的方法体!

执行结果:
bb
bbb
aa
aaa

2、异步方法阻塞的解决方案

2.1 使用线程池执行同步阻塞

from fastapi import FastAPI, Query, Request, Response
import uvicorn
from concurrent.futures import ThreadPoolExecutor
import time
import sys
import os
import asyncio

sys.path.append(os.getcwd())
app = FastAPI(title="bbb")

threadpool=ThreadPoolExecutor(max_workers=3)

############## 【ok】放线程池中:不会阻塞主线程~ ##################
@app.get("/ver2")
async def ver2(request:Request):
    msg=request.query_params.get("msg")

    loop=asyncio.get_event_loop()  # 拿到主线程的事件循环(事件循环可以看做“方法间”的异步)

    task={
        "msg":msg
    }
    def handle_task():
        print("task recieved:", task["msg"])
        result=task["msg"].lower()
        time.sleep(8)
        return result
    
    # 在主线程的事件循环里等待异步结果,因为事件循环是针对“所有方法间”的,所以是在主线程里
    result=await loop.run_in_executor(threadpool, handle_task)  
    print("task ends:", result, asyncio.get_event_loop)
    return Response(result)

if __name__ == "__main__":
    uvicorn.run(app,
                host="0.0.0.0",
                port=9351)  

2.2 使用await 异步等待

2.2.1 异步方法内部使用同步阻塞

【方法间】:A、B、C、D四个请求在异步执行,遵循单线程下的协程异步;
【方法内】:存在同步阻塞,导致遇到阻塞时不会主动让出线程,必须等待当前方法整体执行完(包括先阻塞结束,再执行该方法体内阻塞后的代码)才会让出线程。

2.2.2 异步方法内部使用异步阻塞

【方法间】:不变
【方法内】:异步阻塞,当某个请求调用执行到异步调用代码(await)时,会主动让出线程,失去抢占线程的资格,由其他异步方法抢占。
(1)抢占到线程的协程(异步请求下的异步方法)继续这种操作,即遇到异步阻塞就让出线程;
(2)一旦某个方法异步阻塞结束,它就会恢复重新抢占线程的资格;
(3)当某个方法体全部执行完后,就独立返回,不受其他未完成的方法影响。

from fastapi import FastAPI
import uvicorn
# from concurrent.futures import ThreadPoolExecutor
import time
import sys
import os
import asyncio

sys.path.append(os.getcwd())
app = FastAPI(title="ttt")

@app.get("/a")
async def A(a: int):
    # 任务1
    a = a+10
    print(f"A任务1:{str(a)}")
    # 任务2
    a = a+1
    print(f"A任务2:{str(a)}")
    # 任务3
    # time.sleep(8)  # 同步阻塞8s
    await asyncio.sleep(8)  # 异步阻塞8s
    print(f"A任务3:8s睡好了")
    # 任务4
    a = a+1
    print(f"A任务4:{str(a)}")

@app.get("/b")
async def B(a: int):
    # 任务1
    a = a+20
    print(f"B任务1:{str(a)}")
    # 任务2
    a = a+1
    print(f"B任务2:{str(a)}")
    # 任务3
    # time.sleep(4)  # 同步阻塞4s
    await asyncio.sleep(4)  # 异步阻塞4s
    print(f"B任务3:4s睡好了")
    # 任务4
    a = a+1
    print(f"B任务4:{str(a)}")

@app.get("/c")
async def C(a: int):
    # 任务1
    a = a+30
    print(f"C任务1:{str(a)}")
    # 任务2
    a = a+1
    print(f"C任务2:{str(a)}")
    # 任务3
    a = a+1
    print(f"C任务3:{str(a)}")
    # 任务4
    a = a+1
    print(f"C任务4:{str(a)}")

@app.get("/d")
async def D(a: int):
    # 任务1
    a = a+40
    print(f"D任务1:{str(a)}")
    # 任务2
    a = a+1
    print(f"D任务2:{str(a)}")
    # 任务3
    a = a+1
    print(f"D任务3:{str(a)}")
    # 任务4
    a = a+1
    print(f"D任务4:{str(a)}")

if __name__ == "__main__":
    uvicorn.run(app,
                host="0.0.0.0",
                port=9351)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值