在Node.JS中,事件循环的可预测性是怎样的?

时间:2021-03-05 23:53:23

If you observe simple client-side JS like this, it's intuitive that if we run into setTimeouts, the time length (in this example's case, 0 ms) determine what is the interval AFTER which the JS runtime actually adds this to the queue as explained here.

如果您像这样观察简单的客户端JS,那么很明显,如果我们运行setTimeouts,那么时间长度(在本例中为0 ms)将决定JS运行时实际将其添加到队列的时间间隔。

In Node.JS, however, if I have some api invocation that is fundamentally async (like fs.readFile()) and in the same codebase if I have a simple setTimeout, my understanding is that the event loop will start reading the file, and if has been read, it goes ahead and queues it up so that the appropriate callback can be fired once the main node thread isn't doing any sequential operations. My question is that does this notion of "adding the setTimeout callback" only AFTER the specific timeout still hold (in contrast to class client-side JS). Specifically, here is an example:

在节点。JS,然而,如果我有一些api调用,根本上是异步的(比如fs.readFile())和在同一个代码库如果我有一个简单的setTimeout,我的理解是,事件循环将开始阅读文件,如果被读取,它继续和队列,以便适当的回调被解雇一次主节点线程不做任何顺序操作。我的问题是,只有在特定的超时仍然保持之后(与类客户端JS相反)才会“添加setTimeout回调”。具体来说,这里有一个例子:

const fs = require('fs');
// Set timeout for 2 secs
setTimeout(function() { 
  console.log('Timeout ran at ' + new Date().toTimeString()); 
}, 2000);
// Read some file
fs.readFile('sampleFile.txt', function(err, data) {
   if(err) {
     throw err;
   }
   console.log(data.toString() + " " + new Date().toTimeString();
}
var start = new Date();
console.log('Enter loop at: '+start.toTimeString());
// run a loop for 4 seconds
var i = 0;
// increment i while (current time < start time + 4000 ms)
while(new Date().getTime() < start.getTime() + 4000) {
  i++;
}
console.log('Exit loop at: ' +new Date().toTimeString() +'. Ran '+i+' iterations.');

The output i get for this is:

我得到的输出是:

Enter loop at: 18:22:14 GMT-0700 (PDT) Exit loop at: 18:22:18 GMT-0700 (PDT). Ran 33980131 iterations. Timeout ran at 18:22:18 GMT-0700 (PDT) sampleFileContents 18:22:18 GMT-0700 (PDT)

在18:22:14 GMT-0700 (PDT)退出循环在18:22:18 GMT-0700 (PDT)。跑33980131次迭代。超时时间为18:22:18 GMT-0700 (PDT) sampleFileContents 18:22:18 GMT-0700 (PDT)

Does this mean that somehow the setTimeout callback+message was placed on the event loop queue BEFORE the file could be read completely? This tells me 1 of 3 things: Either the setTimeout callback+message was placed on the queue ready to be fired after 2 secs and the next available tick. OR the time to actually read the sampleFile.txt took more than 2 seconds. OR the sampleFile.txt was read quickly, but somehow it wasn't placed ahead of the setTimeout in the event loop queue.

这是否意味着setTimeout callback+消息以某种方式放在事件循环队列中,然后才能完全读取文件?这告诉我三件事中的一件:setTimeout回调+消息被放在队列上,准备在2秒后和下一个可用的滴答声之后被触发。或者实际读取sampleFile的时间。txt需要超过2秒。或sampleFile。txt被快速读取,但不知何故它没有放在事件循环队列的setTimeout之前。

Am i using the right mental model for think about this? I'm trying to get a deeper understanding of node's internals but without having to dive through the libuv/libeio C-code. I've tried playing around with the timeout, and interestingly enough when I set the timeout to greater than 4000 ms, it appears that in my output, I always print out the sampleFileContents before actually printing what time the timeout ran at.

我用正确的思维模式来思考这个问题吗?我试图深入了解node的内部结构,但不需要深入研究libuv/libeio C-code。我尝试过使用超时,有趣的是,当我将超时设置为大于4000 ms时,在我的输出中,我总是在实际打印超时运行的时间之前打印出sampleFileContents。

1 个解决方案

#1


3  

There is a gotcha. The following lines of code are synchronous and blocking.

有一个问题。以下代码行是同步和阻塞的。

// increment i while (current time < start time + 4000 ms)
while(new Date().getTime() < start.getTime() + 4000) {
  i++;
}

Which means that the event loop was hijacked by it and never got to function as you would expect.

这意味着事件循环被它劫持了,并且永远不能像您预期的那样运行。

The settmeout printing before fileread could mean that at the point of time just before the loop started the timer was already set, but the fileread events were not yet added. I added some more code to verify this idea.

在fileread之前的settmeout打印可能意味着在循环启动之前,计时器已经设置好,但是fileread事件还没有添加。我添加了一些代码来验证这个想法。

var readStream = fs.createReadStream('sampleFile.txt');

  readStream.on('open', function () {
    console.log('Read started ' + new Date().toTimeString());
  });

  readStream.on('data', function(data) {
  });

  readStream.on('end', function(err) {
   console.log('Read end ' + new Date().toTimeString());
  });   

setTimeout(function() {
  console.log('Timeout ran at ' + new Date().toTimeString());
}, 2000);

var start = new Date();
console.log('Enter loop at: '+start.toTimeString());

var i = 0;
while(new Date().getTime() < start.getTime() + 4000) {
  i++;
}

console.log('Exit loop at: ' +new Date().toTimeString() +'. Ran '+i+' times.');

The output is :

的输出是:

Enter loop at: 22:54:01 GMT+0530 (IST)
Exit loop at: 22:54:05 GMT+0530 (IST). Ran 34893551 times.
Timeout ran at 22:54:05 GMT+0530 (IST)
Read started 22:54:05 GMT+0530 (IST)
Read end 22:54:05 GMT+0530 (IST)

Which proves my theory that they never ran concurrently. As to why this happened I believe that fs events need at least one tick to be queued and sent correctly. But timeouts are added instantly. Since you locked the event loop before the fileread event could be added, it was queued after timeout handler after the loop ended.

这证明了我的理论,它们从来没有同时运行。至于发生这种情况的原因,我认为fs事件至少需要一个标记才能被排队并正确发送。但超时是即时添加的。由于您在添加fileread事件之前锁定了事件循环,所以在循环结束后,它在超时处理程序之后排队。

You can try running your code without the loop, the output will be

您可以尝试在没有循环的情况下运行代码,输出将是

Enter loop at: 22:57:15 GMT+0530 (IST)
Exit loop at: 22:57:15 GMT+0530 (IST). Ran 0 iterations.
 22:57:15 GMT+0530 (IST)
Timeout ran at 22:57:17 GMT+0530 (IST)

if read finishes first.

如果先读完成。

#1


3  

There is a gotcha. The following lines of code are synchronous and blocking.

有一个问题。以下代码行是同步和阻塞的。

// increment i while (current time < start time + 4000 ms)
while(new Date().getTime() < start.getTime() + 4000) {
  i++;
}

Which means that the event loop was hijacked by it and never got to function as you would expect.

这意味着事件循环被它劫持了,并且永远不能像您预期的那样运行。

The settmeout printing before fileread could mean that at the point of time just before the loop started the timer was already set, but the fileread events were not yet added. I added some more code to verify this idea.

在fileread之前的settmeout打印可能意味着在循环启动之前,计时器已经设置好,但是fileread事件还没有添加。我添加了一些代码来验证这个想法。

var readStream = fs.createReadStream('sampleFile.txt');

  readStream.on('open', function () {
    console.log('Read started ' + new Date().toTimeString());
  });

  readStream.on('data', function(data) {
  });

  readStream.on('end', function(err) {
   console.log('Read end ' + new Date().toTimeString());
  });   

setTimeout(function() {
  console.log('Timeout ran at ' + new Date().toTimeString());
}, 2000);

var start = new Date();
console.log('Enter loop at: '+start.toTimeString());

var i = 0;
while(new Date().getTime() < start.getTime() + 4000) {
  i++;
}

console.log('Exit loop at: ' +new Date().toTimeString() +'. Ran '+i+' times.');

The output is :

的输出是:

Enter loop at: 22:54:01 GMT+0530 (IST)
Exit loop at: 22:54:05 GMT+0530 (IST). Ran 34893551 times.
Timeout ran at 22:54:05 GMT+0530 (IST)
Read started 22:54:05 GMT+0530 (IST)
Read end 22:54:05 GMT+0530 (IST)

Which proves my theory that they never ran concurrently. As to why this happened I believe that fs events need at least one tick to be queued and sent correctly. But timeouts are added instantly. Since you locked the event loop before the fileread event could be added, it was queued after timeout handler after the loop ended.

这证明了我的理论,它们从来没有同时运行。至于发生这种情况的原因,我认为fs事件至少需要一个标记才能被排队并正确发送。但超时是即时添加的。由于您在添加fileread事件之前锁定了事件循环,所以在循环结束后,它在超时处理程序之后排队。

You can try running your code without the loop, the output will be

您可以尝试在没有循环的情况下运行代码,输出将是

Enter loop at: 22:57:15 GMT+0530 (IST)
Exit loop at: 22:57:15 GMT+0530 (IST). Ran 0 iterations.
 22:57:15 GMT+0530 (IST)
Timeout ran at 22:57:17 GMT+0530 (IST)

if read finishes first.

如果先读完成。