Home > Web Front-end > JS Tutorial > body text

Node.js event loop

高洛峰
Release: 2016-11-18 12:50:01
Original
1237 people have browsed it

What is an Event Loop

The event loop allows Node.js to perform non-blocking I/O operations - despite the fact that JavaScript is single-threaded - by handing operations off to operators when possible implemented by the system kernel.

Since most modern system kernels are multi-threaded, the kernel can handle multiple operations performed in the background. When one of the operations completes, the kernel tells Node.js, and the corresponding callback is added to the poll queue and eventually executed. This topic explains more details about this later.

Event loop

Node.js will initialize the event loop at the beginning and process the target script. The script may make asynchronous API calls, scheduled tasks or process.nextTick(), and then start the event loop.

The following table briefly describes the sequence of operations of the event loop.

Node.js event loop

Note: Each box represents a stage in the event loop.

Each stage has a first-in, first-out (FIFO) queue of callback functions that need to be executed. At the same time, each stage is special. Basically, when the event loop reaches a certain stage, the operations unique to that stage will be performed, and then the callbacks in the queue of that stage will be executed until the queue is empty or the execution limit is reached. . At this time, the event loop will enter the next stage and the cycle will repeat.

Since these operations may generate more scheduled task operations, and new events processed in the polling phase will be added to the kernel queue, new polling events will be added when the polling events are processed. Therefore, long-term callback tasks will cause the polling phase to exceed the timer threshold. See the timers and poll sections for details.

Note: There are slight contradictions in the implementation of Windows and Unix/Linux, but it does not affect the description just now. The most important parts are there. There are actually seven or eight stages, but what we're focusing on -- what Node.js actually uses -- is the ones above.

Phases Overview

Timers (timers): This stage executes setTimeout() and setInterval() planned callbacks;

I/O callbacks: Execute almost all close exceptions that occur Callbacks, callbacks scheduled by timers and setImmediate();

idle, prepare (idle, prepare): internal use only;

polling (poll): Get new I/O events; nodejs will proceed appropriately at this time Blocking;

check: callback to setImmediate();

close callbacks: for example socket.on('close', ... );

Between event loop runs, Node.js checks Is there any asynchronous I/O or timer that is waiting? If not, clear it and end it.

Phase details

Timers (timers)

The purpose of timers is to allow the specified callback function to be executed after a certain threshold. The specific execution time is not necessarily that precise threshold. The timer callback will be executed as soon as the specified time has passed. However, the operating system's schedule or the execution of other callbacks may delay the execution of the callback.

Note: Technically, the polling phase controls the execution timing of the timer.

For example, you set an operation to be performed after 100ms, and then the script starts to perform a file read operation that takes 95ms:

var fs = require('fs');

function someAsyncOperation (callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);


// do someAsyncOperation which takes 95 ms to completesomeAsyncOperation(function () {

  var startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});
Copy after login

When the event loop enters the polling phase, the queue is empty (fs. readFile() has not yet completed), so time will continue to pass until the fastest timer needs to be executed. After 95ms, fs.readFile() has finished reading the file, and its callback is added to the polling queue. This callback needs to be executed for 10ms. When this callback is executed and there are no more callbacks in the queue, the event loop sees the most recently expired timer and then returns to the timers phase to execute the previous timer callback.

In this example, 105ms passed between defining the timer and executing the callback.

Note: In order to prevent the polling phase from lasting too long, libuv will set an upper limit for polling depending on the operating system.

I/O callbacks

This stage performs some callbacks for system operations such as TCP errors. For example, if a TCP socket receives an ECONNREFUSED error when trying to connect, some *nix systems will wait to report this error. This will be queued in this stage.

Poll

The polling phase has two main functions:

1, execute the timer script that has expired, and then

2, process the events in the polling queue.

When the event loop enters the polling phase but no timer is found:

If the polling queue is not empty, the event loop will iterate the callback queue and execute the callback synchronously until the queue is empty or reaches the upper limit (as mentioned above) (the upper limit is set based on the operating system).

如果轮询队列是空的:


如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;

如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。

一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。

检查(check)

这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。

setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。

基本上,随着代码的执行,事件循环会最终进入到等待状态的轮询阶段,可能是等待一个连接、请求等。然而,如果有一个setImmediate() 设置了一个回调并且轮询阶段空闲了,那么事件循环会进入到检查阶段而不是等待轮询事件。 ---- 这车轱辘话说来说去的

关闭事件的回调(close callbacks)

如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。

setImmediate() vs setTimeout()

这两个很相似,但调用时机会的不同会导致它们不同的表现。

setImmediate() 被设计成一旦轮询阶段完成就执行回调函数;

setTimeout() 规划了在某个时间值过后执行回调函数;

这两个执行的顺序会因为它们被调用时的上下文而有所不同。如果都是在主模块调用,那么它们会受到进程性能的影响(运行在本机的其他程序会影响它们)。

例如,如果我们在非 I/O 循环中运行下面的脚本(即在主模块中),他俩的顺序是不固定的,因为会受到进程性能的影响:

// timeout_vs_immediate.jssetTimeout(function timeout () {
  console.log(&#39;timeout&#39;);
},0);

setImmediate(function immediate () {
  console.log(&#39;immediate&#39;);
});
Copy after login

$ node timeout_vs_immediate.js

timeout

immediate


$ node timeout_vs_immediate.js

immediate

timeout

但是如果把它们放进 I/O 循环中,setImmediate() 的回调总是先执行:

// timeout_vs_immediate.jsvar fs = require(&#39;fs&#39;)

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log(&#39;timeout&#39;)
  }, 0)
  setImmediate(() => {
    console.log(&#39;immediate&#39;)
  })
})
Copy after login

$ node timeout_vs_immediate.js

immediate

timeout


$ node timeout_vs_immediate.js

immediate

timeout

setImmediate() 比 setTimeout() 优势的地方是 setImmediate() 在 I/O 循环中总是先于任何定时器,不管已经定义了多少定时器。

process.nextTick()

理解 process.nextTick()

你可能已经注意到了 process.nextTick() 没有在上面那个表格里出现,虽然它确实是一个异步API。这是因为它技术上不属于事件循环。然而,nextTickQueue 会在当前操作结束后被处理,不管是在事件循环的哪个阶段。

回头看看之前那个表格,你在某个阶段的任何时候调用它,它的所有回调函数都会在事件循环继续进行之前得到处理。有时候这会导致比较糟糕的情况,因为它允许你用递归调用的方式去“阻塞” I/O,这会让事件循环无法进入到轮询阶段。

为什么要允许这样

部分是因为 Node.js 的设计哲学:API 应该总是异步的,即使本不需要是异步。

blablabla,后面几段看的我有点尴尬+晕。既尴尬又晕是觉得这几段说的有点啰嗦,而且举的例子不合适。例子要么是同步的,不是异步的。要么是例子里的写法完全可以避免,比如应该先添加 'connect' 事件监听再进行 .connect() 操作;又或者变量声明最好放在变量使用之前,可以避免变量的提前声明和当时赋值的麻烦。

难道是我没理解里面的秘辛?

process.nextTick() vs setTimeout()

这两个函数有些相似但是名字让人困惑:

process.netxtTick() 在事件循环的当前阶段立即生效;

setImmediate() 生效是在接下来的迭代或者事件循环的下一次tick;

本质上,它们的名字应该互换一下。process.nextTick() 比 setImmediate() 更“立刻”执行,但这是个历史问题没法改变。如果改了,npm上大堆的包就要挂了。

我们推荐开发者在所有情况下都使用 setImmediate() 因为它更显而易见(reason about),另外兼容性也更广,例如浏览器端。

为什么使用 process.nextTick()

有两大原因:

允许用户处理错误,清理不需要的资源,或许在事件循环结束前再次尝试发送请求;

必须让回调函数在调用栈已经清除(unwound)后并且事件循环继续下去之前执行;

下面的两个例子都是类似的,即在 line1 派发事件,却在 line2 才添加监听,因此监听的回调是不可能被执行到的。

于是可以用 process.nextTick() 使得当前调用栈先执行完毕,也即先执行 line2 注册事件监听,然后在 nextTick 派发事件。

const EventEmitter = require(&#39;events&#39;);
const util = require(&#39;util&#39;);

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function () {
    this.emit(&#39;event&#39;);
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on(&#39;event&#39;, function() {
  console.log(&#39;an event occurred!&#39;);
});
Copy after login

翻译总结:

 

这篇文章写的不太简练,也可能为了有更多的受众吧,我感觉车轱辘话比较多,一个意思要说好几遍。

 

从编程应用的角度简单来说:

 

Node.js 中的事件循环大概有七八个阶段,每个阶段都有自己的队列(queue),需要等本阶段的队列处理完成后才进入其他阶段。阶段之间会互相转换,循环顺序并不是完全固定的 ,因为很多阶段是由外部的事件触发的。

 

其中比较重要的是三个:

 

定时器阶段 timers:
定时器阶段执行定时器任务(setTimeOut(), setInterval())。

轮询阶段 poll:

          轮询阶段由 I/O 事件触发,例如 'connect','data' 等。这是比较重/重要的阶段,因为大部分程序功能就是为了 I/O 数据。

          本阶段会处理定时器任务和 poll 队列中的任务,具体逻辑:


如果有 setImmediate(),终止轮询阶段并进入检查阶段去执行;

如果没有 setImmediate(),那么就查看有没有到期的定时器,有的话就回到定时器阶段执行回调函数;

处理到期的定时器任务,然后

处理队列任务,直到队列空了或者达到上限

如果队列任务没了:


检查阶段 check:

          当轮询阶段空闲并且已经有 setImmediate() 的时候,会进入检查阶段并执行。

 

比较次要但也列在表格中的两个:

 

I/O 阶段:

          本阶段处理 I/O 异常错误;

'close'事件回调:

          本阶段处理各种 'close' 事件回调;

 

关于 setTimeout(), setImmediate(), process.nextTick():

 

setTimeout()           在某个时间值过后尽快执行回调函数;

setImmediate()       一旦轮询阶段完成就执行回调函数;

process.nextTick()   在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前” ;

 

优先级顺序从高到低: process.nextTick() > setImmediate() > setTimeout()

注:这里只是多数情况下,即轮询阶段(I/O 回调中)。比如之前比较 setImmediate() 和 setTimeout() 的时候就区分了所处阶段/上下文。

 

 

另:

 

关于调用栈,事件循环还可以参考这篇文章:

https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/

 

这篇文章里对事件任务区分了大任务(macro task) 、小任务(micro task),每个事件循环只处理一个大任务 ,但会处理完所有小任务。

这一点和前面的文章说的不同。

examples of microtasks:

process.nextTick

promises

Object.observe

examples of macrotasks:

setTimeout

setInterval

setImmediate

I/O


Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!