NodeJS Promise代码是否存在竞态条件风险?多人游戏场景问询
问题描述
我正在用NodeJS开发一款小型多人游戏。当玩家加入时,我希望将其加载到请求的位置;若该位置已被占用,则寻找最近的空闲区域加载。我的实现逻辑简化如下:
function playerJoin(){ let playerTank = new Tank(); this.findEmptyArea(playerTank, 100, 100).then((result) => { if (!result.success) { return; } addObjectToWorld(playerTank, result.x, result.y); } } function findEmptyArea(object, x, y){ return new Promise((resolve, reject) => { // 伪代码:遍历游戏对象检测碰撞,若请求位置空闲则使用,否则寻找附近空闲位置返回 return resolve({success: true, x: freeX, y: freeY}); // 若未找到空闲位置 return resolve({ success: false }); } }
我担心这段代码存在缺陷:当两名玩家通过WebSocket连接时,可能出现两人的findEmptyArea都找到同一空闲位置(如10,10),随后各自的then块执行addObjectToWorld,导致位置重复、游戏崩溃。想请教:Promise resolve后,then代码块会立即执行吗?还是可能被其他玩家的代码抢先执行,从而引发竞态条件?
回答
Great question—this is a super common pitfall in multiplayer game development with Node.js, and your concern is totally valid. Let’s break this down clearly:
首先,关于Promise的then执行时机
Node.js基于单线程事件循环运行。当Promise resolve后,它的then回调会被加入微任务队列,这个队列会在当前同步代码执行完毕后立即执行。所以单独看一个Promise的then回调,它会在resolve后尽快运行。
但问题的核心不在then的执行时机上——而是你代码里「检测空闲位置」和「将玩家加入该位置」这两步不是原子操作,中间存在可被打断的空隙。
竞态条件为什么会发生
举个具体的执行顺序:
- 玩家A的
findEmptyArea运行,检测到(10,10)空闲,resolve Promise,对应的then回调被排入微任务队列等待执行。 - 在玩家A的
then回调还没运行时,Node.js的事件循环处理了玩家B的WebSocket连接请求,玩家B的findEmptyArea也检测了(10,10)——此时玩家A还没把坦克加入世界,所以这个位置依然显示空闲,于是玩家B的Promise也resolve了(10,10)。 - 接下来两个
then回调依次执行,都调用addObjectToWorld传入(10,10),最终导致两个坦克出现在同一位置,引发游戏异常。
如何解决这个问题
你需要让「检测位置空闲+占用该位置」的流程变成原子操作,避免两个请求在空隙中同时抢占同一位置。这里有几个实用的方案:
1. 在检测流程中临时锁定位置
维护一个临时集合记录正在被处理的「锁定位置」,当findEmptyArea找到空闲位置时,先锁定它再resolve,等玩家成功加入(或流程失败)后再解锁:
// 临时锁定正在处理的位置,避免重复抢占 const lockedPositions = new Set(); function findEmptyArea(object, x, y){ return new Promise((resolve, reject) => { let targetX, targetY; // 先检查请求位置是否空闲且未被锁定 if (isPositionFree(x, y) && !lockedPositions.has(`${x},${y}`)) { targetX = x; targetY = y; lockedPositions.add(`${targetX},${targetY}`); } else { // 寻找附近空闲且未被锁定的位置 targetX = findNearbyFreePosition(x, y); if (targetX === null) { return resolve({ success: false }); } lockedPositions.add(`${targetX},${targetY}`); } resolve({success: true, x: targetX, y: targetY}); }); } function playerJoin(){ let playerTank = new Tank(); this.findEmptyArea(playerTank, 100, 100) .then((result) => { if (!result.success) return; try { // 双重保险:加入前再次检查位置是否空闲 if (isPositionFree(result.x, result.y)) { addObjectToWorld(playerTank, result.x, result.y); } else { // 处理意外情况,比如重新寻找位置 console.log("位置被意外占用,正在重试..."); // 可在此处重新调用findEmptyArea } } finally { // 无论成功失败,都解锁位置 lockedPositions.delete(`${result.x},${result.y}`); } }) .catch(err => { // 出错时清理锁定的位置 if (result?.x && result?.y) { lockedPositions.delete(`${result.x},${result.y}`); } console.error("玩家加入失败:", err); }); }
2. 对玩家加入请求排队处理
用异步队列让玩家加入请求串行执行,确保前一个玩家的「检测+加入」流程完成后,再处理下一个玩家的请求。你可以用async库的队列工具,也可以自己实现简单队列:
// 简单的异步队列,保证玩家加入流程串行执行 let joinQueue = Promise.resolve(); function playerJoin(){ let playerTank = new Tank(); // 将当前玩家的加入逻辑加入队列 joinQueue = joinQueue.then(async () => { const result = await this.findEmptyArea(playerTank, 100, 100); if (result.success) { addObjectToWorld(playerTank, result.x, result.y); } }).catch(err => { console.error("玩家加入失败:", err); }); }
3. 在addObjectToWorld中做最终校验
在addObjectToWorld内部再次检查位置是否空闲,若已被占用则抛出错误,让上层逻辑处理重试或提示玩家:
function addObjectToWorld(object, x, y){ if (!isPositionFree(x, y)) { throw new Error(`位置(${x},${y})已被占用`); } // 执行添加对象的逻辑... }
然后在playerJoin中捕获这个错误,进行重试或其他处理。
内容的提问来源于stack exchange,提问作者user8008276

