node.js代码可以导致竞争条件吗?

问题描述 投票:19回答:6

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

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

node.js race-condition
6个回答
17
投票

不。这是真的,你不能在单线程,非I / O执行程序上有竞争条件。

但是node.js主要是因为它的非阻塞编程方式。非阻塞意味着将侦听器设置为响应事件,您可以在等待此响应时执行其他操作。

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

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

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


16
投票

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

我错误地认为你无法在Node.js中获得竞争条件,因为它具有单线程特性,但只要你在节点之外使用共享资源(例如文件系统中的文件),你就会陷入竞争状态。当我试图理解这个时,我在这个问题上发布了这个问题的一个例子:node.js readfile woes

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


12
投票

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

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();

7
投票

没有.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);
}

6
投票

竞争条件仍然可能发生,因为它们实际上与线程无关,而是在对事件时序和序列做出假设时,因此线程只是一个例子。

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');

该程序应该向客户发送其请求的大小。如果你测试它,似乎工作正确。但它实际上是基于隐式假设,即请求开始和结束事件之间没有任何反应。如果有两个或更多并发客户端,它将无法工作。

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


2
投票

Yes. It can.

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

The case

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

The solution

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');
}
© www.soinside.com 2019 - 2024. All rights reserved.