使用setTimeout同步Node.js中的循环 - 奇怪的,显然是故意的行为

时间:2017-03-23 18:24:37

标签: javascript node.js asynchronous settimeout

我正在调查我们在生产中运行的一段表现不佳的Node.js代码,我看到了这个奇怪的小范例,故意创建一个无限循环。我希望这是一个反模式,但无论如何,当我玩它时,我发现了一些奇怪的行为。

基本思想是我们有一个192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] init 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] password 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query 192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] terminate 函数可以完成一些不确定的长时间工作,然后调用它的回调,以及一个outer函数也可以做一些不确定的工作(涉及一个数据库)连接),但仅在调用inner之后。因此,理想情况下,两个功能都会在15秒内完成,进入睡眠状态,然后重新启动以重新同步环境,但当然并不总是会发生这种情况。

由于一些原因,这似乎很明显,这是一个糟糕的设计,但无论如何,我发现以下玩具示例的行为确实令人惊讶。

setTimeout(function() {outer(inner)}, someDelay)

当我们运行它时,外部函数每秒“完成其工作”,导致每2秒向堆栈添加一个额外的'use strict'; var outer = function(callback) { setTimeout(doWork, 1000); function doWork() { console.log('Did outer work.'); callback(); } } var inner = function() { setTimeout(function() {outer(inner)}, 2000); process.stdin.resume(); process.stdin.once('data', function(data) { console.log('Got ' + data); process.stdin.pause(); console.log('Did inner work.'); }); } outer(inner); 调用(我想)。但是,对outer(inner)的每次调用都会阻塞等待inner()(这个想法是模拟数据库锁定)。如果我在向stdin发送任何内容之前等待outer()被调用5次,那么该堆栈上的所有5个stdin实例将处理相同的数据。

我真的很惊讶 - 我想我只希望一个函数调用接受第一个输入inner()而另外四个要阻止。同样,这是一个糟糕的设计,但是 - 问题:这是可取的吗?有人故意让stdin和JS callstack以这种方式行事,还是其他语言功能的副作用?

或者这是否有意义,我只是以错误的方式思考它?

提前致谢,我很抱歉这是一个模糊的。

2 个答案:

答案 0 :(得分:2)

  

我希望它是一种反模式

不,它没有错。大多数类似服务器的应用程序使用无限循环进行编码 - 您希望它们永远处理连接(即,直到您杀死它或崩溃)。

  

导致每2秒向堆栈添加一个额外的外部(内部)调用(我想)。

没有。 setTimeout是异步的,这意味着它立即返回,其余代码运行完成,然后在新的新调用堆栈上执行预定的回调。堆栈不会以这种半递归模式增长。

  

但是,对inner()的每次调用都会在等待stdin时阻塞(这个想法是模拟数据库锁)。

不,once()不会阻止。它只安装一个处理程序,该事件将在下次发生事件时执行一次。

  

如果我等到在向stdin发送任何内容之前调用outer()5次之后,我真的很惊讶,相同的数据由堆栈上的所有5个inner()实例处理。

是的 - 有5个data事件处理程序在等待事件,并且在事件被触发时它们都会被输入执行。

如果你想在继续递归之前阻塞直到事件发生,你需要将setTimeout安排在data事件监听器中安排下一次读取:

function inner() {
    process.stdin.resume();
    process.stdin.once('data', function(data) {
        console.log('Got ' + data);
        process.stdin.pause();
        console.log('Did inner work.');
        setTimeout(function() {outer(inner)}, 2000);
    });
}

您甚至可以在此省略超时并直接致电outer(inner);

答案 1 :(得分:1)

行为并不令人惊讶:您正在将多个单独的处理程序连接到stdin data事件,因此当事件运行时,它会调用每个处理程序。它完全是这样的:

process.stdin.once('data', function(data) {
    console.log("callback1: " + data);
});
process.stdin.once('data', function(data) {
    console.log("callback2: " + data);
});
process.stdin.once('data', function(data) {
    console.log("callback3: " + data);
});

如果你运行它,然后键入内容并按Enter键,所有三个回调都会收到该事件,因为毕竟所有三个回调都是为该事件注册的。

这正是inner正在做的事情:挂钩data事件以进行多次回调。

这个版本显示了挂钩到data事件的处理程序的数量(当事情实际排队时),可能会让事情变得更清晰:

'use strict';

var handlerCounter = 0;

var outer = function(callback) {
    console.log("outer called, queue doWork for 1s from now");
    setTimeout(doWork, 1000);

    function doWork() {
        console.log('doWork called');
        callback();
    }
}

var inner = function() {
    console.log('Queueing outer(inner) call for 2s from now');
    setTimeout(outer, 2000, inner);
    process.stdin.resume();
    ++handlerCounter;
    console.log('Adding another handler to the `data` event, total will be: ' + handlerCounter);
    process.stdin.once('data', function(data) {
        --handlerCounter; // Since `once` removed it
        console.log('Got ' + data + ', handlers now: ' + handlerCounter);
        process.stdin.pause();
    });
}

outer(inner);
相关问题