【问题标题】:Can node.js code result in race conditions?node.js 代码会导致竞争条件吗?
【发布时间】:2014-02-21 16:14:24
【问题描述】:

根据我的阅读,当不同的线程尝试更改共享变量时会出现竞争条件,这可能导致这些线程的任何串行执行顺序都不可能出现的值。

但是 node.js 中的代码在单线程中运行,那么,这是否意味着用 node.js 编写的代码没有竞争条件?

【问题讨论】:

标签: node.js race-condition


【解决方案1】:

没有。 Node.js 没有由上下文切换引起的竞争条件;但是,您仍然可以编写一个 node.js 程序,其中以意外顺序发生的异步事件会导致状态不一致。

例如,假设您有两个函数。第一个通过 WebSocket 发送消息,并在回调中保存回复。第二个函数删除所有已保存的回复。按顺序调用函数并不能保证消息列表为空。在进行异步编程时,考虑所有可能的事件顺序很重要。

编辑:这是一些示例代码

var messages = [];

...

io.sockets.on('connection', function (socket) {
    socket.emit('ask', { question: 'How many fish do you have?' });
    socket.on('reply', function (data) {
        messages.push(data);
    });
    ...
    wipe();
});

function wipe() {
    setTimeout(function() {
        messages = [];
    }, 500);
}

【讨论】:

  • 竞争条件确实在客户端和node.js程序之间。请注意,给定客户端行为,node.js 程序的输出是已知的。
  • @Guilro 这是一个很好的观点。我将这个问题解释为“Node.js 程序可以表现出竞争条件吗?”答案是肯定的。如果预期的问题是“没有 I/O 的纯 JS 应用程序可以有竞争条件吗?”答案是不。我认为前者是一个更有用的问题,因为大多数 Node.js 应用程序都会有 I/O。
  • @Guilro 等一下。由于套接字使用 Internet,因此了解客户端行为是不够的。您还必须了解网络和操作系统行为。
  • 是的,当然 :) 无论如何,您对预期的问题是正确的,这就是我投票给您的答案的原因。这只是为了提供精确度。
【解决方案2】:

竞争条件仍然可能发生,因为它们实际上与线程无关,但在对事件时间和顺序做出假设时,线程只是其中的一个示例。

Node.js 是单线程的,但仍然是并发的,并且可能出现竞争条件。例如:

var http = require('http');

var size;

http.createServer(function (req, res) {
  size = 0;

  req.on('data', function (data) {
    size += data.length;
  });

  req.on('end', function () {
    res.end(size.toString());
  })

}).listen(1337, '127.0.0.1');

这个程序应该向客户发送他们请求的大小。如果您对其进行测试,似乎可以正常工作。但它实际上是基于隐含的假设,即请求开始和结束事件之间没有发生任何事情。如果有 2 个或更多并发客户端,它将无法工作。

这里发生这种情况是因为size 变量是共享的,很像两个线程共享一个变量时。您可以考虑一个抽象的“异步上下文”,它很像线程,但它只能在某些点暂停。

【讨论】:

  • 好的!但是,在这种情况下,如果size 变量在createServer 回调中,那么每个用户都会获得size 的单独副本,对吧?
  • 我不同意。它们与线程和 I/O 有关,这在概念上等同于在许多线程上共享程序的工作流。您不能在单线程、非 I/O 执行程序上出现竞争条件(我的意思是一个程序只是接受参数并返回退出值)。
  • 这里有另一个线程作用于工作流,它是客户端。如果您将所有程序放在一个主循环中检查新的请求队列,然后在请求中嵌套一个循环检查新数据,那么将不再有竞争条件。这是因为另一个线程可以(通过事件)调用请求处理工作流的一小步,只要它想要存在并发和竞争条件。
  • 您所描述的不是竞争条件。您正在描述 Node.js 中的共享状态以及对它的误解。
  • 从某种意义上说,由于服务器中的共享状态,我们可以说客户端之间存在竞争条件,但服务器本身确实没有竞争条件。
【解决方案3】:

没有。确实,您不能在单线程、非 I/O 执行程序上出现竞争条件。

但是 node.js 的速度主要是因为它的非阻塞编程方式。非阻塞意味着设置一个响应事件的监听器,你可以在等待这个响应的同时做一些别的事情。

为什么? 因为获取响应的工作是在另一个线程上完成的。 数据库,文件系统,在另一个线程上运行,客户端显然在另一台计算机上运行,​​您的程序工作流可以依赖 它的回应。

严格来说,node.js 在一个线程上运行,但您的程序工作流程,包括 I/O(数据库、文件系统)、客户端和所有内容,都在多个线程上运行。

因此,如果您请求将某些内容添加到数据库中,然后只发送删除请求而不等待第一个请求的响应,那么仍然可能存在竞争条件。 如果数据库与 node.js 在同一个线程中运行,则不会出现竞争条件,并且请求只是立即执行的函数调用。

【讨论】:

  • 竞争条件与线程无关。线程会导致竞争条件,因为它们交错执行代码。异步调用也做同样的事情,所以竞争条件是可能的。
  • 没有不可预知 I/O 的异步调用不会导致竞争条件。如果单线程异步代码导致竞争条件,那总是因为涉及到外部资源。
  • @AbiusX 如果你能给出一个代码示例就好了!
  • @jillro 你也一样,如果有代码示例就好了!
【解决方案4】:

是的。一旦您开始共享资源,Node.js 就会遇到竞争条件。

我还错误地认为您无法在 Node.js 中获得竞争条件,因为它是单线程性质的,但是一旦您使用节点外部的共享资源(例如来自文件系统的文件),您就可以进入比赛条件。当我试图理解这一点时,我在这个问题中发布了一个关于这个问题的例子:node.js readfile woes

Node.js 与其他环境的不同之处在于您有一个 JavaScript 执行线程,因此只有一个 JavaScript 实例在运行您的代码(这与有许多线程执行您的应用程序代码的线程环境相反同时。)

【讨论】:

    【解决方案5】:

    是的。可以的。

    当您使用cluster 模块初始化多个工作器时,Nodejs 中的竞争条件是可行的。

    案例

    var cluster = require('cluster');
    var fs = require('fs');
    if(cluster.isMaster){
        for(var i=0;i<4;i++){
            cluster.fork();
        }
    }else{
        fs.watch('/path/to/file',function(){
            var anotherFile = '/path/to/anotherFile';
            fs.readFile(anotherFile,function(er,data){
                 if(er){
                     throw er;
                 }
                 data = +data+1;
                 fs.writeFile(anotherFile,data,function(er){
                     if(er){
                         throw er;
                     }
                     fs.readFile(anotherFile,function(er,newData){
                         if(er){
                             throw er;
                         }
                         console.log(newData); //newData is now undetermined
                     });
                 });
            });
        });
    }
    

    每当您更改监视文件时,4 个工作人员将同时执行处理程序。此行为会导致未确定的newData

    解决办法

    if(cluster.isMaster){
        var lock = {};
        var timer = setInterval(function(){
            if(Object.keys(cluster.workers).length >= 4){
                return clearInterval(timer);
            }
            //note that this lock won't 100% work if workers are forked at the same time with loop.
            cluster.fork().on('message',function(id){
                 var isLocked = lock[id];
                 if(isLocked){
                     return console.log('This task has already been handled');
                 }
                 lock[id] = 1;
                 this.send('No one has done it yet');
            });
        },100);
    }else{
         process.on('message',function(){
            //only one worker can execute this task
            fs.watch('/path/to/file',function(){
                var anotherFile = '/path/to/anotherFile';
                fs.readFile(anotherFile,function(er,data){
                     if(er){
                         throw er;
                     }
                     data = +data+1;
                     fs.writeFile(anotherFile,data,function(er){
                         if(er){
                            throw er;
                         }
                         fs.readFile(anotherFile,function(er,newData){
                             if(er){
                                 throw er;
                             }
                             console.log(newData); //newData is now determined
                         });
                     });
                });
            });
         });
         //ask the master for permission
         process.send('watch');
    }
    

    【讨论】:

      【解决方案6】:

      是的,竞争条件(在共享资源的意义上由于事件的顺序而具有不一致的值)仍然可能发生在任何有可能导致其他代码运行的暂停点的地方(在任何行处都有线程),以这段完全是单线程的异步代码为例:

      var accountBalance = 0;
      
      async function getAccountBalance() {
          // Suppose this was asynchronously from a database or something
          return accountBalance;
      };
      
      async function setAccountBalance(value) {
          // Suppose this was asynchronously from a database or something
          accountBalance = value;
      };
      
      async function increment(value, incr) {
          return value + incr;
      };
      
      async function add$50() {
          var balance, newBalance;
          balance = await getAccountBalance();
          newBalance = await increment(balance, 50);
          await setAccountBalance(newBalance);
      };
      
      async function main() {
          var transaction1, transaction2;
          transaction1 = add$50();
          transaction2 = add$50();
          await transaction1;
          await transaction2;
          console.log('$' + await getAccountBalance());
          // Can print either $50 or $100
          // which it prints is dependent on what order
          // things arrived on the message queue, for this very simple
          // dummy implementation it actually prints $50 because
          // all values are added to the message queue immediately
          // so it actually alternates between the two async functions
      };
      
      main();
      

      此代码在每个等待处都有暂停点,因此可能会在错误的时间在两个函数之间进行上下文切换,产生“$50”而不是预期的“$100”,这与维基百科的竞赛条件示例基本相同在线程中,但有明确的暂停/重新进入点。

      就像线程一样,尽管您可以使用锁(又名互斥锁)之类的东西来解决此类竞争条件。所以我们可以像线程一样防止上述竞争条件:

      var accountBalance = 0;
      
      class Lock {
          constructor() {
              this._locked = false;
              this._waiting = [];
          }
      
          lock() {
              var unlock = () => {
                  var nextResolve;
                  if (this._waiting.length > 0) {
                      nextResolve = this._waiting.pop(0);
                      nextResolve(unlock);
                  } else {
                      this._locked = false;
                  }
              };
              if (this._locked) {
                  return new Promise((resolve) => {
                      this._waiting.push(resolve);
                  });
              } else {
                  this._locked = true;
                  return new Promise((resolve) => {
                      resolve(unlock);
                  });
              }
          }
      }
      
      var account = new Lock();
      
       async function getAccountBalance() {
          // Suppose this was asynchronously from a database or something
          return accountBalance;
      };
      
      async function setAccountBalance(value) {
          // Suppose this was asynchronously from a database or something
          accountBalance = value;
      };
      
      async function increment(value, incr) {
          return value + incr;
      };
      
      async function add$50() {
          var unlock, balance, newBalance;
      
          unlock = await account.lock();
      
          balance = await getAccountBalance();
          newBalance = await increment(balance, 50);
          await setAccountBalance(newBalance);
      
          await unlock();
      };
      
      async function main() {
          var transaction1, transaction2;
          transaction1 = add$50();
          transaction2 = add$50();
          await transaction1;
          await transaction2;
          console.log('$' + await getAccountBalance()); // Now will always be $100 regardless
      };
      
      main();
      

      【讨论】:

      • 对于第一个代码,您有意引入了竞争条件。交易 1 = 添加 50 美元();交易 2 = 添加 50 美元();等待交易1;等待交易2;就像 Promise.all(transaction1, transaction2) 你应该这样做以避免它:await add$50();等待加$50();
      • 重点是证明如果事情同时发生,那么是的,您仍然可以导致与线程类似的竞争条件。所以是的,我故意引入了一个竞争条件来证明这一点。你可能会说有一个 API,你可以在其中做一些事务性的事情作为一个不同的例子,但我不希望这个例子更复杂。
      • 这段代码中还没有竞争条件吗?在您检查if (this._locked) 和您采取的操作之间,状态可能会发生变化,因此您认为获得锁是安全的,但另一个线程在您确定没有锁之后才抓住锁。两个线程可能正在尝试几乎在同一时间获得锁。如果我误解了什么,请告诉我。我是并发新手,这就是我找到这篇文章的原因。
      • 不,这段代码根本不是多线程的,事实上它完全在一个线程中运行。并发和线程实际上是不同的概念,实际上并发是多个任务交错工作的能力。线程/并行虽然是事情实际上同时运行的地方。我的示例纯粹涉及并发并且不会遇到线程问题,因为交错点是明确的(只要有“等待”)。虽然某些环境确实为 Javascript 添加了真正的线程,但大多数环境通过并发公开它,因此不会发生线程式竞争条件
      • 作为一个特定的例子,运行 Javascript 的 Web 浏览器中的线程使用 Worker 对象公开。与Worker 通信的方式只有两种,SharedArrayBuffer 对象或MessageChannels。当使用SharedArrayBuffer 时,绝对会发生真正的竞争条件,但是它们的范围仅限于这些对象并且不能中断任意代码。当使用MessageChannel 对工作的响应排队等待作为事件触发时,这些事件仅在当前没有 JS 运行时触发,因此使用MessageChannel 时不会发生基于线程的竞争条件。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-09-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-09-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多