为什么用 async 异步的 FastAPI 阻塞了所有请求?

为什么用 async 异步的 FastAPI 阻塞了所有请求?

将 def 改成 async def 之后

这是一个简单的 hello 路由

# file: app.py

from fastapi import FastAPI

app = FastAPI()
@app.get("/hello")
def hello_world():
    print("Normal request")
    return "Hello World"

从同步编程过来的同学,写了这样的代码:
请求 10s 后响应客户端

# need improve
@app.get("/sync")
def read_item():
    print("Receive sync request")
    time.sleep(10)
    return "sync Hello World!"

在收到 /sync 响应之前,服务器仍然可以正常响应 /hello 请求。

为了跟上异步编程的潮流,这位同学把代码改成:

@app.get("/async")
async def read_item():
    print("Receive async request")
    time.sleep(10)
    return "async Hello World!"

然而这次收到 /sync 响应之前,服务器已经无法接收任何请求!

异步和同步路由的处理差异

源码解析,分析 FastAPI routing.py 的 get_request_handler,有三个阶段需要由 FastAPI 执行用户定义的函数:

  1. 遍历执行 Depends 依赖
  2. 执行用户定义的 endpoint 函数
  3. 据用户定义的 endpoint 类型,决定序列化响应同步还是异步
def get_request_handler(
    dependant: Dependant,
    ... # 忽略其他参数
    dependency_overrides_provider: Optional[Any] = None) -> Callable[[Request], Coroutine[Any, Any, Response]]:
    assert dependant.call is not None, "dependant.call must be a function"
    is_coroutine = asyncio.iscoroutinefunction(dependant.call)
    is_body_form = body_field and isinstance(body_field.field_info, params.Form)
    ... # 忽略部分代码
        # ---- 执行依赖入口 ----
        solved_result = await solve_dependencies(...)
        values, errors, background_tasks, sub_response, _ = solved_result
        if errors:
            raise RequestValidationError(errors, body=body)
        else:
            # ---- 执行路由入口 ----
            raw_response = await run_endpoint_function(
                dependant=dependant, values=values, is_coroutine=is_coroutine
            )

            ... # 忽略部分代码
            # ---- 序列化入口 ---- 
            response_data = await serialize_response(
                ... # 忽略其他参数
                is_coroutine=is_coroutine,
            )
            ... # 忽略部分代码
            return response

    return app

再往下走一层,发现了这样的共同点:

同步的端点跑在 run_in_threadpool ,而异步的方法(看上去)直接顺序执行

# 执行依赖
async def solve_dependencies(
....
):	    ....
        elif is_coroutine_callable(call):
            solved = await call(**sub_values)
        else:
            solved = await run_in_threadpool(call, **sub_values)

# 执行路由函数
async def run_endpoint_function(
    *, dependant: Dependant, values: Dict[str, Any], is_coroutine: bool
) -> Any:
	...
    if is_coroutine:
        return await dependant.call(**values)
    else:
        return await run_in_threadpool(dependant.call, **values)

# 序列化
async def serialize_response(...) -> Any:
    ...
        if is_coroutine:
            value, errors_ = field.validate(response_content, {}, loc=("response",))
        else:
            value, errors_ = await run_in_threadpool(
                field.validate, response_content, {}, loc=("response",)
            )

这会有什么问题?

为何阻塞?

回过头看 Python 异步编程文档

事件循环(event_loop)在线程(通常是主线程)中运行,并执行其线程中的所有回调和任务。

当任务在 event_loop 中运行时,同一线程中不能运行其他任务

不应(SHOULD NOT)直接调用阻塞(Blocking IO)或 CPU 密集(CPU-Bound)代码。

例如,如果一个函数执行 1 秒的 CPU 密集型计算,则所有并发的 asyncio 任务和 IO 操作都会延迟 1 秒。

读取文件、发送网络请求、读取数据库这类 IO 操作,使用异步库,Python 将不等待中间的 IO 操作完成,而是暂停当前的任务,返回一个 coroutine,处理接下来的操作。

当我们将上方调用同步 time.sleep 方法简单地改成 async 方法,event_loop 执行 time.sleep 将等待 10s 而不是暂停执行其他任务。

在最初版本的同步方法,因为调用 run_in_threadpool ,实际是在另一个线程执行同步阻塞操作。

改成 async 变成在同一个线程执行阻塞操作,当只有一个 worker 时,其他请求自然就无法执行了。

异步编程的最佳实践

  1. 异步方法中应当尽量使用异步库,将同步方法简单加 async 修饰可能起到反作用
  2. 异步方法中,无法使用异步库的 IO 操作,应当使用 event_looprun_in_executor 方法
  3. 异步方法中,存在 CPU 密集操作时,调用 run_in_executor 方法应当使用进程池作为 executor
  4. 开发过程中,使用loop.set_debug(enabled=True) 可以方便找出执行时间长的阻塞操作。
# 需在创建 FastAPI 之前
loop = asyncio.get_event_loop()
loop.set_debug(enabled=True)

app = FastAPI()

在本文中,请求 /async 接口就会产生类似下方的日志:

INFO:asyncio:<Server sockets=(<asyncio.TransportSocket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 50000)>,)> is serving
Receive sync request
INFO:     127.0.0.1:51930 - "GET /async HTTP/1.1" 200 OK
WARNING:asyncio:
Executing <Task...run_asgi() done, 
defined at .../h11_impl.py:392> result=None created at .../http/h11_impl.py:240> 
took 10.014 seconds

time.sleep 换成 await asyncio.sleep(10) 日志便消失了,说明没有阻塞 event_loop 的 IO 操作了。

WeChat Official Account: 程序员的碎碎念

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值