为什么用 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 执行用户定义的函数:
- 遍历执行 Depends 依赖
- 执行用户定义的 endpoint 函数
- 据用户定义的 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 时,其他请求自然就无法执行了。
异步编程的最佳实践
- 异步方法中应当尽量使用异步库,将同步方法简单加 async 修饰可能起到反作用
- 异步方法中,无法使用异步库的 IO 操作,应当使用
event_loop
的run_in_executor
方法 - 异步方法中,存在 CPU 密集操作时,调用
run_in_executor
方法应当使用进程池作为 executor - 开发过程中,使用
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: 程序员的碎碎念