You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Discord.py音乐机器人快速执行两次play命令返回None崩溃问题

Discord.py音乐机器人连续执行play命令崩溃问题

我正在开发一个discord.py音乐机器人,整体运行正常,但碰到个烦人的bug:快速连续执行两次play命令(该命令通过yt_dlp查询YouTube视频)时,第一条命令会因为返回None崩溃,第二条却能正常运行。我对asyncio不太熟,怀疑问题出在asyncio.to_thread()上。

崩溃代码片段

@classmethod
async def parse_from(cls, data):
    thread = asyncio.to_thread(ytdl.extract_info, data, download=False)
    result = await thread
    if 'entries' in result: #<= 这里崩溃,因为result是None
        try:
            data = result['entries'][0]
        except IndexError:
            raise commands.UserInputError('未找到任何结果,请尝试使用不同的搜索词。')
    filename = data['url']
    return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_opts), data=data)

报错回溯

argument of type 'NoneType' is not iterable
  File "D:\Bureau\CODE\Python\Bot-Discord\cogs\voice.py", line 51, in parse_from
    if 'entries' in data:
  File "D:\Bureau\CODE\Python\Bot-Discord\cogs\voice.py", line 158, in play
    song = await YT_Song.parse_from(song_query)
  File "D:\Bureau\CODE\Python\Bot-Discord\main.py", line 44, in on_message
    await bot.process_commands(message)
  File "D:\Bureau\CODE\Python\Bot-Discord\main.py", line 143, in <module>
    bot.run(config.private_token)
TypeError: argument of type 'NoneType' is not iterable

问题原因

yt-dlp的extract_info方法不是线程安全的,如果多个线程共享同一个ytdl实例并发调用该方法,会导致内部状态冲突,其中一个调用会返回None。asyncio.to_thread()每次调用都会启动新线程,当快速连续执行play命令时,两个线程同时操作同一个ytdl实例,就会触发这个问题。

修复方案

方案1:为每个请求创建独立的yt-dlp实例

避免全局复用ytdl实例,每次调用parse_from时新建实例,确保线程隔离:

@classmethod
async def parse_from(cls, data):
    # 每次调用都初始化新的yt-dlp实例
    ytdl_instance = yt_dlp.YoutubeDL(ytdl_opts)
    result = await asyncio.to_thread(ytdl_instance.extract_info, data, download=False)
    
    # 先检查result是否为None,避免后续报错
    if result is None:
        raise commands.UserInputError('未找到任何结果,请尝试使用不同的搜索词。')
        
    if 'entries' in result:
        try:
            data = result['entries'][0]
        except IndexError:
            raise commands.UserInputError('未找到任何结果,请尝试使用不同的搜索词。')
    filename = data['url']
    return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_opts), data=data)

方案2:用锁控制并发调用(适合必须复用实例的场景)

如果需要复用ytdl实例来节省资源,可以用asyncio锁确保同一时间只有一个线程调用extract_info:

# 在YT_Song类中定义一个类级别的锁
ytdl_lock = asyncio.Lock()

@classmethod
async def parse_from(cls, data):
    # 使用锁确保串行调用extract_info
    async with cls.ytdl_lock:
        result = await asyncio.to_thread(ytdl.extract_info, data, download=False)
    
    if result is None:
        raise commands.UserInputError('未找到任何结果,请尝试使用不同的搜索词。')
        
    if 'entries' in result:
        try:
            data = result['entries'][0]
        except IndexError:
            raise commands.UserInputError('未找到任何结果,请尝试使用不同的搜索词。')
    filename = data['url']
    return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_opts), data=data)

通用优化:添加None值检查

不管采用哪种方案,都要先对result做None判断,这能大幅提升代码健壮性,避免因返回值异常导致崩溃。

验证效果

修改后快速连续执行两次play命令,两次请求都会正常处理,不会再出现第一条命令崩溃的情况。


内容的提问来源于stack exchange,提问作者Daf'ium

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 02:18:11