如何在discord.py的on_message事件外运行阻塞式长耗时Python脚本?
解决Discord机器人运行阻塞式长耗时任务的方案
你的核心问题是阻塞式的fetch(wallet)占用了机器人的事件循环,导致心跳中断。下面提供三种可行的方案,都能让任务在后台运行,不影响机器人正常工作,且完成后自动发送结果到Discord频道。
方案一:使用asyncio.to_thread(Python 3.9+ 推荐)
这是最简单的实现方式,Python 3.9+内置的asyncio.to_thread可以直接把阻塞函数放到线程池执行,不阻塞主事件循环。
修改后的机器人代码:
import asyncio # 其他必要导入... async def run_fetch_and_report(wallet, channel): try: # 将阻塞的fetch函数放到线程中执行 report = await asyncio.to_thread(fetch, wallet) # 检测完成后发送结果到频道 await channel.send(f'钱包 {wallet} 的作弊检测完成:\n{report}') except Exception as e: await channel.send(f'检测钱包 {wallet} 时出错:{str(e)}') @bot.event async def on_message(message): if message.author == bot.user: return msg = message.content.lower() channel = message.channel if "check wallet" in msg and str(channel) == "gus": wallet = msg.split("check wallet")[1].strip() # 创建后台任务,无需等待,机器人继续处理其他消息 bot.loop.create_task(run_fetch_and_report(wallet, channel)) await message.channel.send(f'已启动对 {wallet} 的作弊检测,完成后会通知你!') if "hello" in msg: await message.channel.send("Hi, where's Shawn?")
方案二:使用线程池(Python 3.8及以下)
如果你的Python版本低于3.9,可以用concurrent.futures.ThreadPoolExecutor手动创建线程池:
修改后的机器人代码:
import asyncio from concurrent.futures import ThreadPoolExecutor # 其他必要导入... # 初始化线程池,根据服务器配置调整最大线程数 executor = ThreadPoolExecutor(max_workers=4) async def run_fetch_and_report(wallet, channel): try: loop = asyncio.get_event_loop() # 在线程池中执行fetch函数 report = await loop.run_in_executor(executor, fetch, wallet) await channel.send(f'钱包 {wallet} 的作弊检测完成:\n{report}') except Exception as e: await channel.send(f'检测钱包 {wallet} 时出错:{str(e)}') @bot.event async def on_message(message): # 其余逻辑同方案一... if "check wallet" in msg and str(channel) == "gus": wallet = msg.split("check wallet")[1].strip() bot.loop.create_task(run_fetch_and_report(wallet, channel)) await message.channel.send(f'已启动对 {wallet} 的作弊检测,完成后会通知你!') # ...
方案三:使用子进程(适合CPU密集型任务)
如果fetch包含大量CPU密集型计算(比如数百万条数据解析),线程会受GIL限制,用子进程可以彻底隔离任务资源:
步骤1:把fetch单独写成脚本(比如cheater_check.py)
import sys # 导入你的fetch函数所在模块 from your_script import fetch if __name__ == "__main__": if len(sys.argv) != 2: print("参数错误:请传入钱包地址") sys.exit(1) wallet = sys.argv[1] try: report = fetch(wallet) print(report) # 将结果输出到标准输出 except Exception as e: print(f"检测错误:{str(e)}", file=sys.stderr) sys.exit(1)
步骤2:修改机器人代码调用子进程
import asyncio # 其他必要导入... async def run_fetch_and_report(wallet, channel): try: # 启动子进程执行检测脚本 process = await asyncio.create_subprocess_exec( "python", "cheater_check.py", wallet, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) # 获取子进程输出 stdout, stderr = await process.communicate() if process.returncode == 0: report = stdout.decode("utf-8") await channel.send(f'钱包 {wallet} 的作弊检测完成:\n{report}') else: error_msg = stderr.decode("utf-8") await channel.send(f'检测钱包 {wallet} 时出错:{error_msg}') except Exception as e: await channel.send(f'启动检测任务失败:{str(e)}') @bot.event async def on_message(message): # 其余逻辑同方案一... if "check wallet" in msg and str(channel) == "gus": wallet = msg.split("check wallet")[1].strip() bot.loop.create_task(run_fetch_and_report(wallet, channel)) await message.channel.send(f'已启动对 {wallet} 的作弊检测,完成后会通知你!') # ...
关键注意事项
- 移除
global wallet:全局变量会被并发请求覆盖,直接将钱包地址传递给后台任务即可。 - 错误处理:必须捕获任务执行中的异常,避免单个任务崩溃影响整个机器人。
- 资源限制:线程池/子进程的数量不要设置过大,避免服务器资源耗尽。
内容的提问来源于stack exchange,提问作者brian gilbert
相关产品推荐
相关产品推荐

