单文件运行Quart与Discord Bot双事件循环的Uvicorn异常问题
我开发了一个包含两个事件循环的Web应用:一个基于Quart微框架的Web应用,一个Discord机器人。为运行双事件循环,我给Discord机器人开了独立线程。用Python解释器直接运行正常,但通过Uvicorn启动Quart时抛出异常。相关代码和报错信息如下:
应用代码(Application.py)
# Application.py import os import sys import quart from quart_discord import DiscordOAuth2Session from Database import InformationLogManager from Bot import Main import threading import asyncio import typelog os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' GUILD_ID = 1208428835960520734 ROLE_ID = 1208433206215315558 Application = quart.Quart('888KingBotDashboard') InformationLogManager = InformationLogManager.InformationLogManager() # 数据库实例 Logger = typelog.get_logger(__name__) Application.config["SECRET_KEY"] = "thebigchickens" Application.config["DISCORD_CLIENT_ID"] = '1208431980954525746' Application.config["DISCORD_CLIENT_SECRET"] = 'ghvM9A2128vrz9vEzXrvOxp1ff28FIEC' Application.config["DISCORD_REDIRECT_URI"] = "http://localhost:8080/callback" DiscordOAuth2Session = DiscordOAuth2Session(Application) @Application.before_serving async def BeforeFirstRequest(): BotThread = threading.Thread(target=Main.RunBot) # 启动Discord机器人 BotThread.start() @Application.route('/') async def index(): return await quart.render_template('index.html') @Application.route('/login') async def Login(): return await DiscordOAuth2Session.create_session() @Application.route('/callback') async def callback(): try: await DiscordOAuth2Session.callback() except Exception: return quart.redirect(quart.url_for("login")) User = await DiscordOAuth2Session.fetch_user() print(User) return await quart.render_template('logged_in_success.html', User = User) @Application.route('/dashboard') async def dashboard(): for Guild in await DiscordOAuth2Session.fetch_guilds(): if Guild.id == GUILD_ID: User = await DiscordOAuth2Session.fetch_user() if await Main.CheckForRole(GuildId = GUILD_ID, RoleId = ROLE_ID, UserId = User.id): InformationLogManager.LogInformation( DiscordUserId = User.id, IPAddress = quart.request.remote_addr, Email = User.email ) return await quart.render_template("dashboard.html", User = User) else: break else: continue return await quart.render_template("access_denied.html"), 401
报错信息
Exception in thread Thread-1 (RunBot): Traceback (most recent call last): File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\disnake\client.py", line 1116, in run Exception in callback Future.set_result(True) handle: <TimerHandle when=527018.265 Future.set_result(True)> Traceback (most recent call last): File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\events.py", line 80, in _run self._context.run(self._callback, *self._args) asyncio.exceptions.InvalidStateError: invalid state loop.run_forever() File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\windows_events.py", line 319, in run_forever INFO: ASGI 'lifespan' protocol appears unsupported. assert self._self_reading_future is None AssertionError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\disnake\client.py", line 133, in _cleanup_loop _cancel_tasks(loop) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\disnake\client.py", line 115, in _cancel_tasks loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 617, in run_until_complete self._check_running() File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 577, in _check_running asyncio.exceptions.CancelledError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\uvicorn\server.py", line 79, in serve await self.startup(sockets=sockets) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\uvicorn\server.py", line 161, in startup server = await loop.create_server( File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 1459, in create_server infos = await tasks.gather(*fs) asyncio.exceptions.CancelledError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 196, in _run_module_as_main return _run_code(code, main_globals, None, File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 86, in _run_code exec(code, run_globals) File "C:\Users\junej\dev\888BotRestructure\venv\Scripts\uvicorn.exe\__main__.py", line 7, in <module> sys.exit(main()) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\click\core.py", line 1157, in __call__ return self.main(*args, **kwargs) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\click\core.py", line 1078, in main rv = self.invoke(ctx) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\click\core.py", line 1434, in invoke return ctx.invoke(self.callback, **ctx.params) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\click\core.py", line 783, in invoke return __callback(*args, **kwargs) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\uvicorn\main.py", line 418, in main run( File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\uvicorn\main.py", line 587, in run server.run() File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\uvicorn\server.py", line 62, in run Client.run("MTIwODQzMTk4MDk1NDUyNTc0Ng.GJL1Ys.WJk760ouaguhQ818OJMu8z9GvKDOzFAA0pBxuk") File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\disnake\client.py", line 1122, in run _cleanup_loop(loop) File "C:\Users\junej\dev\888BotRestructure\venv\lib\site-packages\disnake\client.py", line 137, in _cleanup_loop loop.close() File "C:\Users\junej\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 676, in close raise RuntimeError("Cannot close a running event loop") RuntimeError: Cannot close a running event loop
问题核心是多线程下asyncio事件循环的资源冲突。Uvicorn启动时会创建并严格管理自己的事件循环,而子线程中调用的client.run()会尝试创建独立事件循环,但Windows环境的ProactorEventLoop不支持这种多线程启动模式,且机器人的自动清理逻辑会和Uvicorn的主循环产生冲突。
具体修复步骤:
重构Discord机器人的启动逻辑
不要使用client.run(),而是手动为子线程创建独立事件循环,用loop.run_until_complete(client.start(token))启动机器人。修改Bot.py中的Main.RunBot函数:import asyncio import disnake # 初始化机器人实例(根据你的需求设置intents) Client = disnake.Client(intents=disnake.Intents.all()) # 保存事件循环实例,方便后续跨线程调用 bot_loop = None def RunBot(): global bot_loop # 为子线程创建独立事件循环 bot_loop = asyncio.new_event_loop() asyncio.set_event_loop(bot_loop) try: # 用start代替run,避免自动清理逻辑冲突 bot_loop.run_until_complete(Client.start("你的机器人token")) except KeyboardInterrupt: # 优雅关闭机器人 bot_loop.run_until_complete(Client.close()) finally: bot_loop.close() async def CheckForRole(GuildId, RoleId, UserId): # 原有的角色检查逻辑 guild = Client.get_guild(GuildId) if not guild: return False member = guild.get_member(UserId) return member and any(role.id == RoleId for role in member.roles)调整Quart路由中的跨线程调用
如果需要在Quart的异步路由中调用机器人的异步方法(比如CheckForRole),不能直接await,需要用asyncio.run_coroutine_threadsafe提交到机器人的事件循环执行:@Application.route('/dashboard') async def dashboard(): for Guild in await DiscordOAuth2Session.fetch_guilds(): if Guild.id == GUILD_ID: User = await DiscordOAuth2Session.fetch_user() # 替换原来的await Main.CheckForRole(...) future = asyncio.run_coroutine_threadsafe( Main.CheckForRole(GuildId=GUILD_ID, RoleId=ROLE_ID, UserId=User.id), Main.bot_loop ) # 同步获取结果,或用await asyncio.wrap_future(future)转为异步 has_role = future.result() if has_role: InformationLogManager.LogInformation( DiscordUserId = User.id, IPAddress = quart.request.remote_addr, Email = User.email ) return await quart.render_template("dashboard.html", User = User) else: break else: continue return await quart.render_template("access_denied.html"), 401保留原有的线程启动逻辑
Application.before_serving中的线程启动代码可以保留,确保机器人在Quart服务启动时自动启动。
为什么直接用Python解释器运行正常?
直接运行时,Quart的事件循环在主线程初始化,子线程的事件循环可以正常创建且生命周期管理宽松;而Uvicorn作为ASGI服务器,对事件循环的生命周期管理更严格,Windows的ProactorEventLoop在多线程场景下的兼容性问题被触发,导致异常。
内容的提问来源于stack exchange,提问作者Untitled Master

