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实例调用,目前无法确定是测试方法存在问题,还是出现了不符合预期的进程间数据共享问题。
问题成因与修复方案
核心原因:可变对象隐式共享
该问题由两个设计缺陷共同导致:
- 方块生成逻辑复用了全局/类作用域的方块序列,不同Game实例从同一个序列中获取Mino对象,而非每个Game实例独立生成专属的Mino实例,同一个Mino实例会被多个Game实例操作,本质是可变对象跨实例共享导致的数据污染。
- 基于
inspect.stack的调用方识别逻辑存在多线程竞争问题:当多个线程同时调用同一个Mino实例的方法时,栈帧读取会出现时序错乱,仅能记录到最后一个调用方的uuid,因此才会出现所有日志只有同一个uuid的假象,掩盖了多实例调用的实际情况。
修复步骤
- 确保每个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() # 仅从自身序列取方块
- 替换日志捕获逻辑,调用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
相关产品推荐
相关产品推荐

