简体   繁体   English

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

[英]Synchronizing loop in Node.js with setTimeout - strange, apparently intentional behavior

I was investigating a poorly-behaving piece of Node.js code we've got running in production, and I saw this odd little paradigm to intentionally create an infinite loop. 我正在研究生产中正在运行的性能不佳的Node.js代码段,并且看到了这个奇怪的小范例,有意创建了一个无限循环。 I hope it's an anti-pattern, but either way while I was playing around with it I found some strange behavior. 我希望这是一种反模式,但是在我玩它的任何一种方式中,我都发现了一些奇怪的行为。

The basic idea is that we have an outer function that does some work that takes indeterminately long and then calls its callback, and an inner function that also does some indeterminate work (that involves a database connection), but only after calling setTimeout(function() {outer(inner)}, someDelay) . 基本思想是,我们有一个outer函数,该函数执行不确定的时间,然后调用其回调,以及一个inner函数,也执行某些不确定的工作(涉及数据库连接),但仅在调用setTimeout(function() {outer(inner)}, someDelay) So, ideally, both functions finish within 15 seconds, go to sleep, and then wake back up to re-sync the environment, but of course that's not always what happens. 因此,理想情况下,两个功能都将在15秒内完成,进入睡眠状态,然后唤醒以重新同步环境,但是当然这并不总是这样。

It seems obvious for a few reasons that this is a bad design, but regardless, I found the following toy example's behavior really surprising. 出于某些原因,这似乎是一个不好的设计,这似乎很明显,但是无论如何,我发现以下玩具示例的行为确实令人惊讶。

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

When we run this, the outer function "does its work" every second, resulting in an additional outer(inner) call added to the stack every 2 seconds (I figure). 当我们运行它时,外部函数每秒执行一次“工作”,从而导致每2秒将额外的outer(inner)调用添加到堆栈中(我认为)。 Each of the calls to inner() , though, blocks on waiting for stdin (the idea was to simulate a database lock). 但是,对inner()每次调用都会在等待stdin阻塞(该想法是模拟数据库锁)。 If I wait until outer() is called 5 times before sending anything to stdin , that same data is processed by all 5 instances of inner() on the stack. 如果我等到5次调用outer() ,然后再将任何内容发送到stdin ,堆栈中的inner()的所有5个实例都会处理相同的数据。

I was really surprised by that - I guess I expected just one function call to accept that first input stdin and the other four to block. 我对此感到非常惊讶-我想我只希望有一个函数调用可以接受第一个输入stdin ,而其他四个要阻塞。 Again, this is a bad design, but - the question: Is this desirable? 同样,这是一个不好的设计,但是-问题:这是否合乎需要? Did someone intentionally make setTimeout() and the JS callstack to behave in this way, or is it a side-effect of some other language feature? 有人故意使setTimeout()和JS调用堆栈以这种方式运行,还是其他语言功能的副作用?

Or does this make sense, and I'm just thinking about it the wrong way? 还是有道理,而我只是在以错误的方式思考?

Thanks in advance, and I'm sorry that it's kind of a vague one. 在此先谢谢您,对不起,它有点含糊。

I hope it's an anti-pattern 我希望这是一种反模式

No, there's nothing wrong with it. 不,这没有错。 Most server-like applications are coded using an infinite loop - you want them to handle connections forever (that is, until you kill it or it crashes). 大多数类似服务器的应用程序都是使用无限循环进行编码的-您希望它们永远处理连接(即,直到您杀死它或崩溃)。

resulting in an additional outer(inner) call added to the stack every 2 seconds (I figure). 导致每2秒将额外的外部(内部)调用添加到堆栈中(我认为)。

No. setTimeout is asynchronous, which means it returns immediately, the rest of the code runs to completion, and then the scheduled callback is executed on a fresh new call stack. setTimeout是异步的,这意味着它立即返回,其余代码运行完毕,然后在新的新调用堆栈上执行调度的回调。 The stack does not grow in this semi-recursive pattern. 堆栈不会以这种半递归模式增长。

Each of the calls to inner(), though, blocks on waiting for stdin (the idea was to simulate a database lock). 但是,对inner()的每次调用都会在等待stdin时阻塞(该想法是模拟数据库锁)。

No, once() does not block. 不, once()不会阻塞。 It just installs a handler that will be executed once when the event occurs the next time. 它只是安装一个处理程序,该处理程序将在下次事件发生时执行一次。

I was really surprised by that if I wait until outer() is called 5 times before sending anything to stdin, that same data is processed by all 5 instances of inner() on the stack. 我真的很惊讶,如果我等到5次调用external()之后再将任何内容发送到stdin,则相同的数据将由堆栈上的inner()的所有5个实例处理。

Yes - there's 5 data event handlers waiting for the event, and they all get executed with the input when the event is fired. 是的-有5个data事件处理程序正在等待事件,并且在触发事件时都将使用输入来执行它们。

If you wanted to block until the event occurs before continuing the recursion, you would need to put the setTimeout that schedules the next read inside the data event listener: 如果要在继续递归之前阻塞事件直到发生,则需要将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);
    });
}

You might even want to omit the timeout here and call outer(inner); 您甚至可能想忽略此处的超时,并调用outer(inner); directly. 直。

The behavior isn't surprising: You're hooking up multiple separate handlers to the stdin data event, so when the event runs, it calls each of those handlers. 这种行为不足为奇:您将多个单独的处理程序连接到stdin data事件,因此在事件运行时,它将调用这些处理程序中的每个处理程序。 It's exactly like this: 就像这样:

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

If you run that, then type something and press Enter, all three callbacks receive the event, because after all, all three callbacks are registered for the event. 如果运行该命令,然后键入一些内容并按Enter,则所有三个回调都将接收该事件,因为毕竟所有三个回调都已为该事件注册。

That's exactly what inner is doing: Hooking the data event for multiple callbacks. 这正是inner所做的:将data事件挂接到多个回调中。


This version, showing the number of handlers hooked to the data event (and when things are actually queued), may make things a bit clearer: 这个版本显示挂钩到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);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM