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

Python multiprocessing结合neat-python开发俄罗斯方块AI的异常问题

使用neat-python开发俄罗斯方块AI时出现的跨实例数据异常Bug

复现步骤

  • 将种群规模设置为较大值(>150)
  • 启动训练(进化)流程
  • 游戏因方块意外超出边界崩溃

初步排查情况

已确认tetromino碰撞检测函数无逻辑问题。为定位问题,给游戏对象新增了列表属性存储所有执行过的操作(含失败操作),导出该列表、当前方块、暂存方块、游戏棋盘、已落方块等对象后得到如下异常结果:

>>> actions = [...] # 所有已执行的操作,最后一个应为触发崩溃的操作
>>> len(actions)
92
>>> actions.count('hold')
87
>>> actions.count('down') # 该操作仅由游戏对象线程定时器触发
5
>>> 87 + 5
92
>>> current.direction # 方块默认朝向为'up',且已执行操作中无'rotate'操作
'right'
>>> [piece.direction for piece in unbag] # unbag存储所有已放置/锁定/粘贴到游戏棋盘的方块
['up', 'down', 'up', 'right', 'left']

验证方案与测试结果

初步猜测同时评估48个实例时存在不同游戏实例互相影响的情况。为验证该猜想,给Game类的__init__方法新增uuid属性,赋值为str(uuid4()),同时在Mino类中添加id_log集合,在rotate、move等方法被调用时记录调用方Game实例的uuid,相关代码如下:

Game类修改

class Game:
    def __init__(self, board: np.array=None, network: FFN=None, render=False):
        self.uuid = str(uuid4())

Mino类修改

class Mino:
    def __init__(self, type_: str, pos: Tuple[int, int]=None) -> None:
        if type_ not in 'IJLOSTZ':
            raise ValueError(f'invalid tetromino type {repr(type_)}')
        self.type = type_
        if pos is None:
            pos = SPAWN_POSITIONS[type_]
        self.x: int = pos[0]
        self.y: int = pos[1]
        self.direction = 'up'
        self.id_log = set()
...
    def rotate(self) -> None:
        self._log_id()
        ...
...
    def move(self, board, dir_):
        self._log_id()
        ...
...
    def _log_id(self):
        stack = inspect.stack()
        caller_object = stack[2][0].f_locals['self']
        if isinstance(caller_object, Game):
            self.id_log.add(caller_object.uuid)

测试后得到如下结果:

>>> current.id_log
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}
>>> for p in unbag:
...     print(p.id_log)
...
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}
{'aa7980ed-c3a6-4ab6-8c41-0938e1b78eb1'}

所有方块的操作都仅被同一个uuid的Game实例调用,目前无法确定是测试方法存在问题,还是出现了不符合预期的进程间数据共享问题。

问题成因与修复方案

核心原因:可变对象隐式共享

该问题由两个设计缺陷共同导致:

  1. 方块生成逻辑复用了全局/类作用域的方块序列,不同Game实例从同一个序列中获取Mino对象,而非每个Game实例独立生成专属的Mino实例,同一个Mino实例会被多个Game实例操作,本质是可变对象跨实例共享导致的数据污染。
  2. 基于inspect.stack的调用方识别逻辑存在多线程竞争问题:当多个线程同时调用同一个Mino实例的方法时,栈帧读取会出现时序错乱,仅能记录到最后一个调用方的uuid,因此才会出现所有日志只有同一个uuid的假象,掩盖了多实例调用的实际情况。

修复步骤

  1. 确保每个Game实例初始化时生成独立的方块序列,所有Mino实例仅归属对应Game实例,不与其他实例共享:
# Game类__init__中新增独立方块生成逻辑,禁用全局方块序列
def __init__(self, board: np.array=None, network: FFN=None, render=False):
    self.uuid = str(uuid4())
    self.bag = self._generate_new_bag() # 每个Game实例专属的方块序列
    self.current_mino = self._get_next_mino_from_self_bag() # 仅从自身序列取方块
  1. 替换日志捕获逻辑,调用Mino方法时直接传入Game实例自身的uuid,避免栈帧读取的竞争问题:
# Mino方法修改
def rotate(self, game_uuid: str) -> None:
    self.id_log.add(game_uuid)
    ...

# Game中调用rotate的逻辑调整
self.current_mino.rotate(self.uuid)

修改后重新运行测试,如果单个Mino的id_log出现多个uuid,即可确认对象共享的问题定位正确。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 01:21:03