Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nodejs: event loop #2

Open
redsx opened this issue Jan 19, 2020 · 0 comments
Open

nodejs: event loop #2

redsx opened this issue Jan 19, 2020 · 0 comments
Labels

Comments

@redsx
Copy link
Owner

redsx commented Jan 19, 2020

什么是事件循环

Event loop是一种程序结构,是实现异步的一种机制。这种异步执行的运行机制如下:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。
    为了更好地理解Event Loop,可以参考下图(转引自Philip Roberts的演讲 What the heck is the event loop anyway

image

Node.js的事件循环

当 Node.js 启动后,它会初始化事件轮询;处理已提供的输入脚本,它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环
下面的图表展示了事件循环操作顺序的简化概览:

image

每个阶段都有一个FIFO的回调队列(queue)要执行。而每个阶段有自己的特殊之处,简单说,就是当event loop进入某个阶段后,会执行该阶段特定的(任意)操作,然后才会执行这个阶段的队列里的回调。当队列被执行完,或者执行的回调数量达到上限后,event loop会进入下个阶段

阶段概述

  • timers: 这个阶段执行setTimeout()setInterval()设定的回调。
  • I/O callbacks: 执行被推迟到下一个iteration的 I/O 回调。
  • idle, prepare: 仅内部使用。
  • poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。这个阶段执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

阶段详情

Timer

一个timer指定 可以执行所提供回调 的阈值,而不是用户希望其执行的确切时间。在指定的一段时间间隔后, timer回调会被尽可能早的运行。但系统调度或者其它回调的执行可能会延迟timer回调的执行。

I/O callbacks

这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。

poll

阶段有两个重要的功能:

  1. 执行下限时间已经达到的timers的回调。
  2. 处理 poll 队列里的事件。

当事件循环进入 poll 阶段且 没有timer时 ,将发生以下两种情况之一:

  • 如果 poll 队列 不是空的 event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;。
  • 如果 poll 队列 是空的 ,还有两件事发生:
    • 如果设置了 setImmediate() 回调,则事件循环将结束 poll 阶段,并继续 check 阶段以执行那些被调度的脚本。
    • 如果脚本 未被 setImmediate()设置回调,event loop将阻塞在该阶段等待回调被加入 poll 队,然后立即执行。

一旦 poll 队列为空,event loop将检查已达到时间阈值的timer。如果一个或多个timer达到设定时间,则事件循环将绕回计timer阶段以执行这些timer回调。

check

这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
setImmediate()实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API来设定在 poll 阶段结束后立即执行回调。
通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待请求连接等时间。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待poll events。

close callbacks

如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

举例说明Event loop

🌰setimmediate立即执行?

执行:

const now = Date.now();
setTimeout(() => console.log("------ timer 1000 ------"), 1000);
setTimeout(() => console.log("------ timer 1 ------"), 100);
setImmediate(() => console.log("------ immediate ------"));
while(Date.now() - now < 10000) {}

输出:

*timer*[uv__run_timers]: enter
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: QUEUE NOT EMPTY
*poll*[uv__io_poll]: QUEUE NOT EMPTY
*poll*[uv__io_poll]: QUEUE EMPTY
*timer*[uv__run_timers]: enter
------ timer 1 ------
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: QUEUE NOT EMPTY
*poll*[uv__io_poll]: QUEUE NOT EMPTY
*poll*[uv__io_poll]: QUEUE EMPTY
*poll*[uv__io_poll]: exit
*check*[uv__run_check]: enter
------ immediate ------
*check*[uv__run_check]: exit
*closing*[uv__run_closing_handles]: enter
*closing*[uv__run_closing_handles]: exit
*timer*[uv__run_timers]: enter
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: QUEUE EMPTY
---------- 等待poll ----------

*poll*[uv__io_poll]: exit
*check*[uv__run_check]: enter
*check*[uv__run_check]: exit
*closing*[uv__run_closing_handles]: enter
*closing*[uv__run_closing_handles]: exit
*timer*[uv__run_timers]: enter
------ timer 1000 ------
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: exit
*check*[uv__run_check]: enter
*check*[uv__run_check]: exit
*closing*[uv__run_closing_handles]: enter
*closing*[uv__run_closing_handles]: exit

分析:

  1. 在poll阶段,发现timer 1到达执行阈值,回到timer阶段

一旦 poll 队列为空,event loop将检查已达到时间阈值的timer。如果一个或多个timer达到设定时间,则事件循环将绕回计timer阶段以执行这些timer回调。

  1. 再次循环进入poll阶段,无timer回调达到时间,且设置了setImmediate回调,则结束poll 到check阶段

如果设置了 setImmediate() 回调,则事件循环将结束 poll 阶段,并继续 check 阶段以执行那些被调度的脚本。

  1. 第三次timer时间到达前poll处于阻塞等待回调加入,注意等待是有超时时间的,以上只截取等待一次log

如果脚本 未被 setImmediate()设置回调,event loop将阻塞在该阶段等待回调被加入 poll 队,然后立即执行。

🌰setimmediate真的立即执行

执行:

const fs = require('fs')
const now = Date.now();

fs.readFile(__filename, () => {
    console.log('------ readfile ------')
    setTimeout(() => console.log('------ fs: timer ------'), 0);
    setImmediate(() => console.log('------ fs: immediate ------'));
});

输出:

*timer*[uv__run_timers]: enter
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: QUEUE EMPTY
*poll*[uv__io_poll] timeout: 8099
*poll*[uv__io_poll] nfds: 1
------ readfile ------
*poll*[uv__io_poll]: exit
*check*[uv__run_check]: enter
------ fs: immediate ------
*check*[uv__run_check]: exit
*closing*[uv__run_closing_handles]: enter
*closing*[uv__run_closing_handles]: exit
*timer*[uv__run_timers]: enter
------ fs: timer ------
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit

分析:

  1. 为什么这一次fs: immediate会先执行?
  • 看打印出来的日志可以知道poll阶段回调执行完成之后进入check阶段,check阶段会执行setImmediate回调
  • 然后进入下一轮循环timer执行
  1. 为什么例1中timer会先执行?与本例有什么不同?
  • 例1中之所以会先执行,是因为在执行poll阶段时poll队列为空,检查发现timer到达执行时间,才跳回timer阶段执行
  • 例1中两者都在主模块中运行,本例在回调中运行,poll阶段回调执行完成之后进入check阶段,setImmediate的回调永远先执行。所以说setImmediate真的是立即执行的

🌰setImmediate和process. nextTick()

执行:

setTimeout(() => {
    console.log("------ timer 1 ------")
    process.nextTick(() => console.log("------ nextTick 1 ------"));
}, 1);
setImmediate(() => console.log("------ immediate 2 ------"));
process.nextTick(() => console.log("------ nextTick ------"));

输出:

*timer*[uv__run_timers]: enter
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll] timeout: -1
------ nextTick ------
*timer*[uv__run_timers]: enter
------ timer 1 ------
------ nextTick 1 ------
*timer*[uv__run_timers]: exit
*I/O callbacks*[uv__run_pending]: enter
*I/O callbacks*[uv__run_pending]: exit
*poll*[uv__io_poll]: enter
*poll*[uv__io_poll]: exit
*check*[uv__run_check]: enter
------ immediate 2 ------
*check*[uv__run_check]: exit
*closing*[uv__run_closing_handles]: enter
*closing*[uv__run_closing_handles]: exit

分析:

  1. process.nextTick在当前操作完成后处理,不管目前处于事件循环的哪个阶段,它都会立即执行
  2. setImmediate只能在 check 阶段执行回调

其他补充

以上例子中有涉及修改node源码打印执行日志可以参考 node源码编译&使用

// deps/uv/src/unix/core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  //...
  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    printf("*timer*[uv__run_timers]: enter\n");
    uv__run_timers(loop);
    printf("*timer*[uv__run_timers]: exit\n");

    printf("*I/O callbacks*[uv__run_pending]: enter\n");
    ran_pending = uv__run_pending(loop);
    printf("*I/O callbacks*[uv__run_pending]: exit\n");
    uv__run_idle(loop);
    uv__run_prepare(loop);
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) {
      timeout = uv_backend_timeout(loop);
    }

    printf("*poll*[uv__io_poll]: enter\n");
    uv__io_poll(loop, timeout);
    printf("*poll*[uv__io_poll]: exit\n");

    printf("*check*[uv__run_check]: enter\n");
    uv__run_check(loop);
    printf("*check*[uv__run_check]: exit\n");

    printf("*closing*[uv__run_closing_handles]: enter\n");
    uv__run_closing_handles(loop);
    printf("*closing*[uv__run_closing_handles]: exit\n");
    //...
}

浏览器的事件循环

对比Nodejs事件循环

浏览器的事件循环事表现出的状态与node中大致相同。但是浏览器的有自己的一套事件循环模型。
浏览器至少有一个事件循环,一个事件循环至少有一个任务队列。此外每个事件循环都有一个microtask queue。

An event loop has one or more task queues. A task queue is a set of tasks.

Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.

macrotask(任务队列) & microtask

Macrotasks包含生成dom对象、解析HTML、执行主线程js代码、更改当前URL还有其他的一些事件如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,macrotask代表一些离散的独立的工作。当执行完一个task后,浏览器可以继续其他的工作如页面重渲染和垃圾回收。
Microtasks则是完成一些更新应用程序状态的较小任务,如处理promise的回调和DOM的修改,这些任务在浏览器重渲染前执行。Microtask应该以异步的方式尽快执行,其开销比执行一个新的macrotask要小。Microtasks使得我们可以在UI重渲染之前执行某些任务,从而避免了不必要的UI渲染,这些渲染可能导致显示的应用程序状态不一致。
image

上图看出一些细节:

  1. 一次事件循环只会执行一个macrotask
  2. 一次事件循环却可以处理完所有的microtask,且microtasks都应该在下次渲染前执行完。

举例说明

🌰一个简易事件循环

执行:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

输出:

script start
script end
promise1
promise2
setTimeout

分析:
browser-deom1-excute-animate

🌰复杂的事件循环触发

执行:

//<div class="outer">
//  <div class="inner"></div>
//</div>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

输出:

click
promise
mutate
click
promise
mutate
timeout
timeout

分析:

2020-01-19 20-58-05 2020-01-19 20_59_46

参考文章
JavaScript 运行机制详解:再谈Event Loop
Node.js的event loop及timer/setImmediate/nextTick
html规范.事件循环
深入理解js事件循环机(浏览器篇)
HTML系列:macrotask和microtask
asks, microtasks, queues and schedules

@redsx redsx added the node node label Jan 20, 2020
@redsx redsx changed the title node: event loop nodejs: event loop Mar 23, 2020
@redsx redsx added the browser label Mar 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant