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

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()

关键注意事项

  1. 永远不要在多进程之间传递Discord Client/Message/Member等对象:这些对象包含不可序列化的资源,只能传递它们的ID、字符串等基础数据。
  2. 所有Discord操作必须由Master进程执行:只有Master有活跃的Discord网关连接,Worker只负责处理数据,把操作指令发给Master。
  3. 注意Manager对象的使用:用Manager()创建的dict、list、Value是跨进程共享的,而普通的Queue也是进程安全的,不要混用普通数据结构和多进程数据结构。

内容的提问来源于stack exchange,提问作者Quentin Herique

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:08:44