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

NodeJS Promise代码是否存在竞态条件风险?多人游戏场景问询

关于Node.js多人游戏中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的执行时机上——而是你代码里「检测空闲位置」和「将玩家加入该位置」这两步不是原子操作,中间存在可被打断的空隙。

竞态条件为什么会发生

举个具体的执行顺序:

  1. 玩家A的findEmptyArea运行,检测到(10,10)空闲,resolve Promise,对应的then回调被排入微任务队列等待执行。
  2. 在玩家A的then回调还没运行时,Node.js的事件循环处理了玩家B的WebSocket连接请求,玩家B的findEmptyArea也检测了(10,10)——此时玩家A还没把坦克加入世界,所以这个位置依然显示空闲,于是玩家B的Promise也resolve了(10,10)。
  3. 接下来两个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:14:27