我的NodeJS诺言代码竞争条件安全吗?

时间:2019-04-09 11:55:39

标签: javascript node.js game-development

我目前正在用Node.js编写一款小型多人游戏。

当玩家加入时,我想将他们加载到游戏世界中的请求位置。但是,这个位置可能会被占用,如果这样,我会四处寻找最近的可用空间并将其加载到该位置。

我的方法如下:

    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) => {
          // Psuedo code: code iterates over game objects testing for 
          // collisions . if the requested position is free uses that else 
          // finds an empty nearby location and returns it
          return resolve({success: true, x: freeX, y: freeY});

          // fails to find empty location for object
          return resolve({ success: false });
        } 
  }

这不是实际的代码,而是简化的版本,以使其更加清晰。我的问题是这样:

当用户通过Web套接字连接时,playerJoin函数将运行。然后,它会创建一个新的玩家坦克,找到可用区域并返回一个诺言,如果成功,则会将玩家坦克添加到该位置的世界中。

仔细研究一下,我想知道这段代码是否有缺陷。是否有可能在实际上并非免费的位置上调用addObjectToWorld?

如下所示:

  1. player1连接为player1调用的playerJoin
  2. 在player1的playerJoin内调用了
  3. findEmptyArea
  4. 玩家2连接了playerJoin并呼叫了player2
  5. 在player2的playerJoin内调用了
  6. findEmptyArea
  7. 玩家1的
  8. findEmptyArea承诺找到10,10的可用空间,然后 回报承诺。
  9. 玩家2的
  10. findEmptyArea承诺找到了免费的 间隔为10,10并返回promise。

  11. .then()承诺代码块 在player1的playerJoin中(在findEmptyArea之后)运行,通过将该玩家置于10,10 addObjectToWorld

  12. player.Join中的.then()Promise代码块 播放器2运行,通过addObjectToWorld将播放器置于10,10。 游戏崩溃

所以我想我的问题是,当一个诺言解决时,.then代码块会立即运行,而addObjectToWorld会立即运行,还是其他代码可能首先运行(例如另一个玩家也找到了空闲区域) >

谢谢您的帮助

2 个答案:

答案 0 :(得分:0)

由于findEmptyArea是异步的,所以很可能访问外部的东西,对吧?

Node.js是用于执行JS代码的单线程(即使最新版本中具有实验性的多线程功能)。但是Node.JS使用子线程访问外部资源。因此,只有在访问外部资源(例如文件)或执行API请求时,您才可以具有竞争条件。

如果您发出API请求,则您访问的资源将负责确保它仅返回一次true以防止出现竞争情况。

但是,如果您仅检查Promises中的本地对象,则您不应该具有任何竞争条件(只要您不玩setTimeout之类的东西)。 Node.JS事件循环将一次为每个已解决/已拒绝的Promise执行代码。这意味着一旦解决了Promise,then代码块就会被执行。

我发现this article对于事件队列与Promises的组合很有帮助。

您问题的答案取决于findEmptyArea检查空白区域的方式。

一些编码建议

调用returnresolve时不需要使用reject。您可能要使用reject,因为您提到了变量:

this.findEmptyArea(playerTank, 100, 100).then((result) => {
  addObjectToWorld(playerTank, result.x, result.y);    
}).catch((err) => {
  // findEmptyArea failed because the Promise was "rejected"
});

return new Promise((resolve, reject) => {
  // On success call this
  resolve({x: freeX, y: freeY});

  // On failure call this
  reject();
});

答案 1 :(得分:0)

  

所以我想我的问题是,当一个诺言解决时,.then代码块会立即运行,而addObjectToWorld会立即运行,还是其他代码可能首先运行(例如另一个玩家也找到了空闲区域) >

看这个例子:

'use strict';

async function work(i) {
  const interval = setInterval(() => { console.log('interval', i); }, 1);
  setImmediate(() => console.log('immediate', i));
  process.nextTick(() => console.log('nexttick', i));
  await findEmptyArea(i);
  addObjectToWorld(i, interval);
}

function addObjectToWorld(k, interval) {
  console.log('add object to the world', k);
  clearInterval(interval);
}

function findEmptyArea(k) {
  console.log('findEmptyArea resolving', k);
  return new Promise((resolve, reject) => {
    console.log('findEmptyArea executing', k);
    for (let i = 0; i < 1000000000; i++) {
      // high computation
    }
    resolve({ success: true, x: 1, y: 1 });
  });
}

for (let x = 0; x < 10; x++) {
  work(x);
}

nextTickaddObjectToWorld之前运行。

  

nodejs事件循环是否可能允许其他东西先运行?

如果没有异步代码(I / O),请看示例,这将基于事件循环而按顺序进行。

  

我把它放在了承诺之内,因为随着新玩家的加入,playerJoin将经常运行,我不想阻止它。

请考虑添加Promiseasync不会在不阻塞事件循环的情况下转换代码:您仍在使用promise阻塞事件循环(请检查{{1} }:它阻止了所有连接用户的事件循环。

为了不阻止事件循环,您应该在worker_thread或子进程中运行高级计算任务。

如果您这样做,事件循环将不会停止,因此您可以服务更多的用户并在需要时扩大规模。

但是,当然,在这种情况下,您应该实现optimistic之类的锁定模式才能预订可用空间。

示例:

for < 1000000000

// worker.js

'use strict';


const { Worker } = require('worker_threads');

async function work(i) {
  const interval = setInterval(() => { console.log('interval', i); }, 1);
  setImmediate(() => console.log('immediate', i));
  process.nextTick(() => console.log('nexttick', i));
  await findEmptyArea(i);
  addObjectToWorld(i, interval);
}

function addObjectToWorld(k, interval) {
  console.log('add object to the world', k);
  clearInterval(interval);
}

function findEmptyArea(k) {
  console.log('findEmptyArea resolving', k);
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: k });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

for (let x = 0; x < 10; x++) {
  work(x);
}