关于在协程内使用 Uvicorn 无法正常开启 Web 服务的分析处理
关于在协程内使用 uvicorn 无法正常开启 web 的分析处理
1. 问题描述
在使用 asyncio.run()
启动协程时,试图同时启动一个基于 FastAPI 和 Uvicorn 的 Web 服务,然而却遇到了 Web 服务无法正常启动的问题。此问题的根本原因是 asyncio.run()
与 uvicorn.run()
在事件循环上存在冲突。
下面是一个简单的 demo,用于描述这个 bug 的产生,当然,下面这个代码会直接 raise 一个错误,而有些复杂情况可能并不会直观地告诉你具体的错误。
示例代码
import asyncio
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Hello, World!"}
async def main():
# 启动 Web 服务
uvicorn.run(app, host="0.0.0.0", port=8001)
if __name__ == "__main__":
asyncio.run(main())
在上述代码中,我们使用 asyncio.run()
启动了 main()
函数,而 main()
函数内部又直接调用了 uvicorn.run()
来启动 Web 服务。运行这个代码时,Web 服务没有正常启动,原因就在于 asyncio.run()
和 uvicorn.run()
都试图管理事件循环,导致冲突。
2. 通过查看 uvicorn 的源码,发现问题根源
查看 uvicorn
的源码可以发现,uvicorn.run()
内部实际上也调用了 asyncio.run()
来启动事件循环。如下:
class Server:
...
def run(self, sockets: list[socket.socket] | None = None) -> None:
self.config.setup_event_loop() # 设置事件循环
return asyncio.run(self.serve(sockets=sockets)) # 使用 asyncio.run 启动事件循环
在 Server.run()
方法中,asyncio.run()
被用来启动 Web 服务的事件循环,这与我们在 main()
中使用 asyncio.run()
启动事件循环的做法产生了冲突。最终导致 Web 服务无法正常运行。
3. 解决方法
为了避免事件循环的冲突,可以通过将 uvicorn.run()
和主服务封装起来,确保它们共享同一个事件循环。我们可以通过 asyncio.create_task()
来启动 uvicorn
服务,而不是使用 asyncio.run()
。这样可以确保两个服务在同一个事件循环中运行。
解决方案代码
import uvicorn
import asyncio
from fastapi import FastAPI
app = FastAPI()
class WebService:
def __init__(self, host="0.0.0.0", port=8001):
self.host = host
self.port = port
async def run(self):
"""启动 Web 服务"""
# 使用 asyncio.create_task 以便让 uvicorn 运行在当前事件循环中
uvicorn_task = asyncio.create_task(self.run_uvicorn())
await uvicorn_task
async def run_uvicorn(self):
"""封装 uvicorn 的启动"""
config = uvicorn.Config(app, host=self.host, port=self.port)
server = uvicorn.Server(config)
await server.serve()
async def main():
# 创建 Web 服务实例
web_service = WebService()
# 启动 Web 服务
web_service_task = asyncio.create_task(web_service.run())
await web_service_task
if __name__ == "__main__":
asyncio.run(main())
关键点:
- 使用
asyncio.create_task()
启动uvicorn
,确保uvicorn
与主服务共享同一个事件循环。 - 封装
uvicorn.run()
代码,通过async
方式启动uvicorn
,而非直接调用asyncio.run()
,避免了事件循环的冲突。
结论
通过封装 uvicorn
并使用 asyncio.create_task()
启动服务,我们可以确保多个异步服务在同一个事件循环中运行,避免了事件循环冲突问题。这种方法可以顺利启动 Web 服务并避免和其他协程产生冲突。