Discord Python Bot多进程报错:无法序列化WeakSet本地对象
解决多进程Discord Bot的Pickle序列化错误
你遇到的AttributeError: Can't pickle local object 'WeakSet.__init__.<locals>._remove'错误,本质是**discord.Client对象(以及它内部依赖的异步网络会话、事件监听WeakSet结构)无法被Python的pickle机制序列化**。而Windows系统下多进程默认用spawn模式创建子进程,会要求把进程的所有参数序列化后传递给子进程,直接传Client实例必然触发这个错误。
核心问题拆解
Discord的客户端是基于异步IO设计的,内部包含大量不可序列化的资源:
- 活跃的aiohttp网络会话(报错里的
Unclosed client session就是这个) - 用WeakSet管理的事件回调函数(就是报错里的
WeakSet.__init__.<locals>._remove本地函数,Python无法pickle本地函数) - 各种与Discord网关的连接状态信息
而且Discord客户端本身就不是为多进程共享设计的,强行传递只会导致资源冲突或序列化失败。
正确的架构重构方案
我们需要把Discord交互逻辑和业务处理逻辑拆分到不同进程:
- Master进程:唯一负责运行
discord.Client,接收Discord消息,把消息的可序列化关键数据放入队列;同时监听Worker进程的指令,执行Discord操作(比如禁言、删除消息)。 - Worker进程:只从队列读取消息数据,处理业务逻辑( spam检测、违禁词过滤),如果需要操作Discord,就把指令放回另一个队列让Master执行。
修改后的代码示例
1. 重构Master类
import asyncio import datetime from multiprocessing import Process, Manager, Queue, cpu_count import discord TOKEN = "你的Bot Token" def get_file_content(path): with open(path, 'r', encoding='utf-8') as f: return [line.strip() for line in f if line.strip()] class Master: def __init__(self): print('Starting Master...') self.client = discord.Client(intents=discord.Intents.all()) # 注意要启用对应的Intents self.manager = Manager() self.user_spam = self.manager.dict() self.user_mute_chat = self.manager.dict() self.spam_t = self.manager.Value('i', 0) self.user_t_spam_muted = self.manager.Value('i', 0) self.request_queue = Queue() self.command_queue = self.manager.Queue() # 新增:Worker给Master发指令的队列 self.cpu = cpu_count() # 注册Discord事件 self.client.event(self.on_ready) self.client.event(self.on_message) # 加载配置 self.forbidden_words_list = self.manager.list(get_file_content('res/forbidden_words.txt')) for line in get_file_content('res/settings.txt'): if 'spamtime=' in line: self.spam_t.value = int(line.replace('spamtime=', '')) # 注意这里要修改Value的value属性 if 'usertimemuted' in line: self.user_t_spam_muted.value = int(line.replace('usertimemuted=', '')) # 启动Worker进程 print('Starting Workers...') for i in range(self.cpu): Worker( self.request_queue, self.command_queue, self.user_spam, self.user_mute_chat, self.spam_t, self.user_t_spam_muted, self.forbidden_words_list ).start() # 启动Discord客户端 print('Connecting to discord...') self.client.run(TOKEN) async def on_ready(self): print(f'Logged in as {self.client.user}') # 启动后台任务处理Worker的指令 self.client.loop.create_task(self.process_commands()) async def on_message(self, message): # 跳过机器人自己的消息 if message.author.bot: return # 把消息的关键信息打包成可序列化的字典(不要传整个Message对象) message_data = { 'author_id': message.author.id, 'author_name': str(message.author), 'content': message.content, 'channel_id': message.channel.id, 'guild_id': message.guild.id if message.guild else None, 'message_id': message.id } self.request_queue.put(message_data) async def process_commands(self): """处理Worker发过来的Discord操作指令""" while True: # 用asyncio.wrap_future把同步队列的get转为异步 command = await asyncio.wrap_future(asyncio.get_event_loop().run_in_executor(None, self.command_queue.get)) try: if command['action'] == 'mute': user_id = command['user_id'] guild_id = command['guild_id'] duration = command['duration'] guild = self.client.get_guild(guild_id) if guild: user = guild.get_member(user_id) if user: # 禁言用户,这里用discord.py的timeout方法 await user.timeout(discord.utils.utcnow() + datetime.timedelta(seconds=duration)) print(f'Muted user {user.name} for {duration}s') elif command['action'] == 'delete_message': channel_id = command['channel_id'] message_id = command['message_id'] channel = self.client.get_channel(channel_id) if channel: message = await channel.fetch_message(message_id) await message.delete() except Exception as e: print(f'Failed to execute command: {e}') self.command_queue.task_done()
2. 重构Worker类
class Worker(Process): def __init__(self, request_queue, command_queue, user_spam, user_mute_chat, spam_t, user_t_spam_muted, forbidden_words_list): super().__init__() self.request_queue = request_queue self.command_queue = command_queue self.user_spam = user_spam self.user_mute_chat = user_mute_chat self.spam_t = spam_t self.user_t_spam_muted = user_t_spam_muted self.forbidden_words_list = forbidden_words_list def run(self): """Worker进程的业务处理逻辑(同步执行,不要用async)""" print(f'Worker {self.pid} started') for data in iter(self.request_queue.get, None): author_id = data['author_id'] content = data['content'] guild_id = data['guild_id'] channel_id = data['channel_id'] message_id = data['message_id'] # -------------------------- # 示例1:违禁词检测 # -------------------------- content_lower = content.lower() if any(word in content_lower for word in self.forbidden_words_list): # 给Master发禁言指令 self.command_queue.put({ 'action': 'mute', 'user_id': author_id, 'guild_id': guild_id, 'duration': self.user_t_spam_muted.value }) # 可选:发删除消息指令 self.command_queue.put({ 'action': 'delete_message', 'channel_id': channel_id, 'message_id': message_id }) # -------------------------- # 示例2:Spam检测逻辑(你自己的代码) # -------------------------- # 这里写你的spam检测逻辑,比如统计用户发送频率 if author_id not in self.user_spam: self.user_spam[author_id] = {'count': 1, 'last_time': datetime.datetime.now()} else: self.user_spam[author_id]['count'] += 1 time_diff = (datetime.datetime.now() - self.user_spam[author_id]['last_time']).seconds if time_diff < self.spam_t.value and self.user_spam[author_id]['count'] >= 5: # 触发spam,发禁言指令 self.command_queue.put({ 'action': 'mute', 'user_id': author_id, 'guild_id': guild_id, 'duration': self.user_t_spam_muted.value }) # 重置计数 self.user_spam[author_id]['count'] = 0 self.user_spam[author_id]['last_time'] = datetime.datetime.now() # 标记任务完成 self.request_queue.task_done()
3. 必须添加的主入口保护
Windows下多进程spawn模式会重新导入脚本,所以必须把Master的实例化放在if __name__ == '__main__':块里,否则会无限创建进程:
if __name__ == '__main__': master = Master()
关键注意事项
- 永远不要在多进程之间传递Discord Client/Message/Member等对象:这些对象包含不可序列化的资源,只能传递它们的ID、字符串等基础数据。
- 所有Discord操作必须由Master进程执行:只有Master有活跃的Discord网关连接,Worker只负责处理数据,把操作指令发给Master。
- 注意Manager对象的使用:用
Manager()创建的dict、list、Value是跨进程共享的,而普通的Queue也是进程安全的,不要混用普通数据结构和多进程数据结构。
内容的提问来源于stack exchange,提问作者Quentin Herique
相关产品推荐
相关产品推荐

