creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

Node.js的event loop及timer/setImmediate/nextTick #26

Open creeperyang opened 7 years ago

creeperyang commented 7 years ago

本文是对Node.js官方文档The Node.js Event Loop, Timers, and process.nextTick()的翻译和理解。文章并不是一字一句严格对应原文,其中会夹杂其它相关资料,以及相应的理解和扩展。

相关资料:

什么是事件循环(Event loop)?

Event loop是什么?

WIKI定义:

In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program.

Event loop是一种程序结构,是实现异步的一种机制。Event loop可以简单理解为:

  1. 所有任务都在主线程上执行,形成一个执行栈(execution context stack)。

  2. 主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后主线程继续执行后续的任务。

  3. 一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。

  4. 主线程不断重复上面的第三步。

对JavaScript而言,Javascript引擎/虚拟机(如V8)之外,JavaScript的运行环境(runtime,如浏览器,node)维护了任务队列,每当JS执行异步操作时,运行环境把异步任务放入任务队列。当执行引擎的线程执行完毕(空闲)时,运行环境就会把任务队列里的(执行完的)任务(的数据和回调函数)交给引擎继续执行,这个过程是一个不断循环的过程,称为事件循环

注意:JavaScript(引擎)是单线程的,Event loop并不属于JavaScript本身,但JavaScript的运行环境是多线程/多进程的,运行环境实现了Event loop。

另外,视频What the heck is the event loop anyway 站在前端的角度,用动画的形式描述了上述过程,可以便于理解。

解释Node.js的Event loop

当Node.js启动时,它会初始化event loop,处理提供的代码(代码里可能会有异步API调用,timer,以及process.nextTick()),然后开始处理event loop。

下面是node启动的部分相关代码:

// node.cc
  {
    SealHandleScope seal(isolate);
    bool more;
    do {
      v8_platform.PumpMessageLoop(isolate);
      more = uv_run(env.event_loop(), UV_RUN_ONCE);

      if (more == false) {
        v8_platform.PumpMessageLoop(isolate);
        EmitBeforeExit(&env);

        // Emit `beforeExit` if the loop became alive either after emitting
        // event, or after running some callbacks.
        more = uv_loop_alive(env.event_loop());
        if (uv_run(env.event_loop(), UV_RUN_NOWAIT) != 0)
          more = true;
      }
    } while (more == true);
  }

Event Loop的执行顺序

下面的示意图展示了一个简化的event loop的操作顺序:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

(图来自Node.js API

图中每个“盒子”都是event loop执行的一个阶段(phase)。

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

Phases Overview 阶段总览

Phases in Detail 阶段详情

timers

一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。

注意:技术上来说,poll 阶段控制 timers 什么时候执行。

注意:这个下限时间有个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

I/O callbacks

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

poll

poll 阶段有两个主要功能:

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

当event loop进入 poll 阶段,并且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:

  1. 如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
  2. 如果 poll 队列为空,则发生以下两件事之一:

    1. 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。
    2. 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。

但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):

1. event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 **timers** 阶段,并执行 **timer** 队列。

check

这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。

setImmediate()实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API 来设定在 poll 阶段结束后立即执行回调。

通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。

close callbacks

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

event loop的一个例子讲述

var fs = require('fs');

function someAsyncOperation (callback) {
  // 假设这个任务要消耗 95ms
  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);

// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {

  var startCallback = Date.now();

  // 消耗 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});

当event loop进入 poll 阶段,它有个空队列(fs.readFile()尚未结束)。所以它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,然后它的回调被加到 poll 的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的 下限时间,然后回到 timers 阶段,执行timer的回调。

所以在示例里,回调被设定 和 回调执行间的间隔是105ms。

setImmediate() vs setTimeout()

setImmediate()setTimeout()是相似的,区别在于什么时候执行回调:

  1. setImmediate()被设计在 poll 阶段结束后立即执行回调;
  2. setTimeout()被设计在指定下限时间到达后执行回调。

下面看一个例子:

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

setImmediate(function immediate () {
  console.log('immediate');
});

代码的输出结果是:

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

是的,你没有看错,输出结果是 不确定 的!

从直觉上来说,setImmediate()的回调应该先执行,但为什么结果随机呢?

再看一个例子:

// timeout_vs_immediate.js
var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

结果是:

$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

很好,setImmediate在这里永远先执行!

所以,结论是:

  1. 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,即随机。
  2. 如果两者都不在主模块调用(即在一个 IO circle 中调用),那么setImmediate的回调永远先执行。

那么又是为什么呢?

int uv_run(uv_loop_t* loop, uv_run_mode mode)源码(deps/uv/src/unix/core.c#332):

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    //// 1. timer 阶段
    uv__run_timers(loop);
    //// 2. I/O callbacks 阶段
    ran_pending = uv__run_pending(loop);
    //// 3. idle/prepare 阶段
    uv__run_idle(loop);
    uv__run_prepare(loop);

    // 重新更新timeout,使得 uv__io_poll 有机会跳出
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    //// 4. poll 阶段
    uv__io_poll(loop, timeout);
    //// 5. check 阶段
    uv__run_check(loop);
    //// 6. close 阶段
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      // 7. UV_RUN_ONCE 模式下会再次检查timer
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

上面的代码看起来很清晰,一一对应了我们的几个阶段。

  1. 首先进入timer阶段,如果我们的机器性能一般,那么进入timer阶段时,1毫秒可能已经过去了(setTimeout(fn, 0) 等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。
  2. 如果没到一毫秒,那么我们可以知道,在check阶段,setImmediate的回调会先执行。
  3. 为什么fs.readFile回调里设置的,setImmediate始终先执行?因为fs.readFile的回调执行是在 poll 阶段,所以,接下来的 check 阶段会先执行 setImmediate 的回调。
  4. 我们可以注意到,UV_RUN_ONCE模式下,event loop会在开始和结束都去执行timer。

理解 process.nextTick()

直到现在,我们才开始解释process.nextTick()。因为从技术上来说,它并不是event loop的一部分。相反的,process.nextTick()会把回调塞入nextTickQueuenextTickQueue将在当前操作完成后处理,不管目前处于event loop的哪个阶段。

看看我们最初给的示意图,process.nextTick()不管在任何时候调用,都会在所处的这个阶段最后,在event loop进入下个阶段前,处理完所有nextTickQueue里的回调。

process.nextTick() vs setImmediate()

两者看起来也类似,区别如下:

  1. process.nextTick()立即在本阶段执行回调;
  2. setImmediate()只能在 check 阶段执行回调。
zColdWater commented 7 years ago

@creeperyang Hi,我想说的是那个105ms的例子,我用js在浏览器当中执行异步代码的原理也能解释的通,那么问题是不是WebAPIs是浏览器中的,在Nodejs中就是libuv,EventLoop只是等待堆栈执行完才把TaskQueue的回调Push到堆栈执行,按照我的理解是,someAsyncOperation方法先完成了,他被push到任务队列中,然后EventLoop判断Stack中空了,执行那剩下10秒,但是过了5s后,Settimeout回调任务完成了,被Push到任务队列中,但是当前Stack正在执行后面那5s不为空,所以EventLoop要等到为空才执行这个回调,所以是105ms。 js在web中的例子:http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D

creeperyang commented 7 years ago

@zColdWater 你理解得应该没什么问题,就是表述有点不是很好懂(比如没有event loop为空的说法)。

下面总结几点关于event loop的:

  1. 在浏览器和Node.js中,JS都是以单线程在运行,当前执行栈必然是执行完(为空) 才会进入下个event loop。
  2. process.nextTickPromise都是microtask,即在进入下个event loop前执行(也可以理解成不是event loop相关的概念)。
  3. setTimeout/setImmediate/io是task,会把回调延迟到后面的某个event loop执行。
  4. node.js中event loop分为不同的阶段,每个阶段有自己的任务。
chn777 commented 7 years ago

那在H5新特征的多线程JS处理代码中,这个异步该如何处理呢?

zColdWater commented 7 years ago

@creeperyang 谢谢,🍻。

creeperyang commented 7 years ago

@chn777 你是指web worker吗?web worker工作在单独的线程,有自己的event loop。

chn777 commented 7 years ago

@creeperyang 是的是的,谢谢了

ChuanTS commented 7 years ago

Hi, 不是很理解poll阶段具体是负责什么业务。 首先,按照我对文章的理解,timers的回调执行是在timers阶段,不过判断却是在poll阶段,一旦条件满足就返回timers阶段,如果这样把timers放在poll后面才合适; 其次,时间回调和IO回调都有相对应的处理阶段,那么poll阶段要处理的回调是什么回调呢。或者,poll不是执行回调,而只是遍历所有的IO,将满足条件的回调填充到timer和IO的队列里去。这么来讲,poll队列对应的事件呢

YDSS commented 7 years ago

@ChuanTS

Julyrainy commented 7 years ago

您好,现在才看到您这篇文章,想请问以下代码中:

setImmediate(function(){
  console.log("setImmediate");
  setImmediate(function(){
    console.log("嵌套setImmediate");
  });
  process.nextTick(function(){
    console.log("nextTick");
  })
});

执行结果:

setImmediate
nextTick
嵌套setImmediate

我在setImmediate中又调用了一个setImmediate,称外层setImmediate为A,内层setImmediate为B,那么我想问,为什么此时B不会紧接着A执行,而是使得其中的nextTick先执行呢?我认为是B应该也被加入了setImmediate所在的队列,应该会继续执行啊,希望能得到您的回复,谢谢!

creeperyang commented 7 years ago

@Julyrainy 简单说,setImmediate 只有在 check 阶段执行,nextTick 在每个阶段的末尾都会执行。即,nextTick 属于 microtask,而setImmediate属于macrotask,希望这可以帮助你理解。

可以联系 #21 一起看。

Julyrainy commented 7 years ago

@creeperyang 多谢您的回复,主要就是macrotask和microtask我不理解。

setImmediate 只有在 check 阶段执行

我的困惑就在于此,在我上方例子中,A执行的时候必然是处于check阶段,A执行时候新注册了一个setImmediate,也就是B,此时B为何不能继续执行?是因为A执行之后直接退出了check阶段吗?

另外看过好多资料说macrotask一次tick只能执行一个,macrotask队列中的下一个task得等到下次tick才能执行,而microtask能执行多次,这是正确的说法吗?

creeperyang commented 7 years ago

@Julyrainy https://nodejs.org/api/timers.html#timers_setimmediate_callback_args

Schedules the "immediate" execution of the callback after I/O events' callbacks. Returns an Immediate for use with clearImmediate().

When multiple calls to setImmediate() are made, the callback functions are queued for execution in the order in which they are created. The entire callback queue is processed every event loop iteration. If an immediate timer is queued from inside an executing callback, that timer will not be triggered until the next event loop iteration.

文档中很清楚地写明了:

  1. 多次调用 setImmediate() 则把回调都放入队列,在 check 阶段都会执行;
  2. setImmediate() 回调里调用setImmediate() ,则放到下次 event loop。

其实这很好理解,一个 event loop 有多个阶段,每个阶段做对应的事,timer 阶段会把需要执行的 回调 (队列)都执行,check 阶段也会把 把需要执行的 回调 (队列)都执行。

creeperyang commented 7 years ago

一个帮助理解的例子:

const async_hooks = require('async_hooks')
const fs = require('fs')

let indent = 0

async_hooks.createHook({
  init(asyncId, type, triggerId) {
    const cId = async_hooks.currentId()
    print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`)
  },
  before(asyncId) {
    print(`${getIndent(indent)}before:  ${asyncId}`)
    indent += 2
  },
  after(asyncId) {
    indent -= 2
    print(`${getIndent(indent)}after:   ${asyncId}`)
  },
  destroy(asyncId) {
    print(`${getIndent(indent)}destroy: ${asyncId}`)
  },
}).enable()

function print(str) {
  fs.writeSync(1, str + '\n');
}

function getIndent(n) {
  return ' '.repeat(n)
}

print('start')

setTimeout(() => {
  print('--outter: timeout1')
  setImmediate(() => {
    print('--inner: setImmediate1')
    process.nextTick(() => {
      print('--inner-inner: nextTick')
    })
  })
  setTimeout(() => {
    print('--inner: setTimeout')
  })
  setImmediate(() => {
    print('--inner: setImmediate2')
  })
  process.nextTick(() => {
    print('--inner: nextTick')
  })
})
process.nextTick(() => {
  print('--outter: nextTick')
})
setTimeout(() => {
  print('--outter: timeout2')
})

print('end')

借助 async_hooks 模块,可以看到以下输出:

//// loop 1
start
Timeout(2): trigger: 1 scope: 1
TIMERWRAP(3): trigger: 1 scope: 1
TickObject(4): trigger: 1 scope: 1
Timeout(5): trigger: 1 scope: 1
end
before:  4
--outter: nextTick
after:   4

///// loop 2
before:  3
  before:  2
--outter: timeout1
    Immediate(6): trigger: 2 scope: 2
    Timeout(7): trigger: 2 scope: 2
    Immediate(8): trigger: 2 scope: 2
    TickObject(9): trigger: 2 scope: 2
  after:   2
  before:  5
--outter: timeout2
  after:   5
after:   3
before:  9
--inner: nextTick
after:   9
destroy: 4
destroy: 2
destroy: 5
destroy: 9

///// loop 3
before:  3
  before:  7
--inner: setTimeout
  after:   7
after:   3
destroy: 7
before:  6
--inner: setImmediate1
  TickObject(10): trigger: 6 scope: 6
after:   6
before:  8
--inner: setImmediate2
after:   8
before:  10
--inner-inner: nextTick
after:   10
destroy: 6
destroy: 8
destroy: 10
destroy: 3

很容易看出,两个 timeout 回调在同一个 loop 执行,两个 setImmediate 同一个 loop 执行。

当我们去除 async_hooks 模块,重复跑这段代码,有时可能会有不同的输出:

start
end
--outter: nextTick
--outter: timeout1
--inner: nextTick
--outter: timeout2
--inner: setTimeout
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick

或:

start
end
--outter: nextTick
--outter: timeout1
--outter: timeout2
--inner: nextTick
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick
--inner: setTimeout

我们知道 nextTick 是 microtask,那么 nextTick 出现在不同位置,告诉我们这几次运行中 event loop 的次数和划分和之前有所不同。

之所以出现这样的情况,应该跟 timer 的时间判断有关。setTimeout(fn) 实质上即 setTimeout(fn, 1),所以在运行环境稍有差异时,可能会有超出 1ms 的运行时间不同,timer 的时间判断也随即不一样。

Julyrainy commented 7 years ago

@creeperyang 万分感谢您细心的回复,我有点清晰了,我在好好研读一下

ruiming commented 7 years ago

@Julyrainy 一次事件循环只执行一次宏任务(task), 然后执行多个微任务(microtask). 这个是浏览器端的事件循环模型, 不适用于 nodejs. 我之前也在关于 nodejs 的事件循环看到这种说法, 这个是错误的. 关于浏览器端的事件循环可以参考 https://html.spec.whatwg.org/multipage/webappapis.html#event-loops . 如果楼主有兴趣的话, 不妨也翻译下.

mygaochunming commented 7 years ago

@creeperyang 我看到nextTick的这种用法:

var http = require('http');

function compute() { // 执行一个cpu密集的任务 // ... process.nextTick(compute); }

http.createServer(function(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World'); }).listen(5000, '127.0.0.1');

compute();

请教一下,这里的nextTick嵌套,是不是在每个“阶段”执行一次?否则岂不是死循环了?

Looking back at our diagram, any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues. This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.

说的是不是这种情况?

creeperyang commented 7 years ago

@mygaochunming 是。但不是每个“阶段”执行一次,而是在该阶段持续执行 nextTick 注册的回调。

mygaochunming commented 7 years ago

@creeperyang 我上面那段代码摘自http://www.cnblogs.com/lengyuhong/archive/2013/03/31/2987745.html 二. cpu高密集代码段

这里岂不是死循环,即文章中说的“假死”。

creeperyang commented 7 years ago

@mygaochunming 你贴的代码的确是死循环,compute 会重复执行。

libin1991 commented 7 years ago

macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 microtasks: Promise, process.nextTick, Object.observe, MutationObserver

image

当一个程序有:setTimeout, setInterval ,setImmediate, I/O, UI渲染,Promise ,process.nextTick, Object.observe, MutationObserver的时候:

1.先执行 macrotasks:I/O -》 UI渲染-》requestAnimationFrame

2.再执行 microtasks :process.nextTick -》 Promise -》MutationObserver ->Object.observe

3.再把setTimeout setInterval setImmediate【三个货不讨喜】 塞入一个新的macrotasks,依次:setTimeout ,setInterval --》setImmediate

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);   
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
结果是:3 4 6 8 7 5 2 1  
Zerxoi commented 6 years ago
const fs = require('fs')
const now = Date.now();

setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}

输出

immediate
readfile

能解释一下是为什么么? 我觉得代码过1s执行完后,事件放入poll阶段的队列,应该首先执行fs.readfile()回调,之后再执行check阶段的setImmediate回调。

Zerxoi commented 6 years ago

答案:How does setTimeout() in Nodejs Event Loop's works?

rueian commented 6 years ago

Hi @Zerxoi

感謝你提供了這麼棒的一個範例,這個範例可以幫助我們更了解 libuv 的 event loop 不過我自己好奇嘗試了一下,發現 stackoverflow 上的這個解釋並不正確

我在 libuv event loop 代碼插入了一堆 printf 觀察了一下,大概至上是改這樣:

  int count = 0;
  while (r != 0 && loop->stop_flag == 0) {
    count++;

    printf("\n\n===== LOOP ROUND %d =====\n", count);

    uv__update_time(loop);

    printf("[uv__run_timers]: enter\n");
    uv__run_timers(loop);
    printf("[uv__run_timers]: exit\n");

    printf("[uv__run_pending]: enter\n");
    ran_pending = uv__run_pending(loop);
    printf("[uv__run_pending]: exit\n");

    printf("[uv__run_idle]: enter\n");
    uv__run_idle(loop);
    printf("[uv__run_idle]: exit\n");

    printf("[uv__run_prepare]: enter\n");
    uv__run_prepare(loop);
    printf("[uv__run_prepare]: exit\n");
    ...

此外,因為 uv__io_poll (kqueue.c) 的實作也很長,我在裡面一些 if else 的分支部分也插入 printf ,像是這樣

      if (ev->filter == EVFILT_READ) {
        printf("[uv__io_poll]: ev->filter == EVFILT_READ\n");
        ...
      }

      if (ev->filter == EV_OOBAND) {
        printf("[uv__io_poll]: ev->filter == EV_OOBAND\n");
        ...
      }

      if (ev->filter == EVFILT_WRITE) {
        printf("[uv__io_poll]: ev->filter == EVFILT_WRITE\n");
        ...
      }

然後編譯 node,執行你給的範例

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

setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}

得到的結果是這樣子


===== LOOP ROUND 1 =====
[uv__run_timers]: enter
[uv__run_timers]: handle->timer_cb
timer
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
immediate
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit

===== LOOP ROUND 2 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit

===== LOOP ROUND 3 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit

===== LOOP ROUND 4 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
readfile
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit

若將 readFile 拿掉:

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

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

則會得到


===== LOOP ROUND 1 =====
[uv__run_timers]: enter
[uv__run_timers]: handle->timer_cb
timer
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
immediate
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit

可以確定的是 readFile 的 callback 確實是在 uvio_poll 這個階段執行的,不是 uv__run_pending 而 LOOP ROUND 1~4 裡面的 uvio_poll 都有在做事情看起來應該是 readFile 產生的 event 沒辦法在一個 LOOP ROUND 裡面做完

creeperyang commented 6 years ago

@rueian 👍

提供的这段 log 对了解 libuv 的处理流程很有帮助!

sunyongjian commented 6 years ago

据说 requestAnimationFrame 不算是 macro task。

https://stackoverflow.com/questions/43050448/when-will-requestanimationframe-be-executed

LiangRenDev commented 6 years ago

Two questions 1 From this article Understand Node.JS eventloop

Misconception 1: The event loop runs in a separate thread than the user code Misconception There is a main thread where the JavaScript code of the user (userland code) runs in and another one that runs the event loop. Every time an asynchronous operation takes place, the main thread will hand over the work to the event loop thread and once it is done, the event loop thread will ping the main thread to execute a callback.

Reality There is only one thread that executes JavaScript code and this is the thread where the event loop is running. The execution of callbacks (know that every userland code in a running Node.js application is a callback) is done by the event loop. We will cover that in depth a bit later.

So it seems there is only one thread in Node.js, do I understand right?

2 文中提到 ‘I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。’ 。 但是原文中的意思是這個階段只是執行系統圖回調,你所説的執行所有回調是什麽意思呢?能都詳細説明,謝謝。

creeperyang commented 6 years ago

@LeonAppDev Q1: JS是单线程的,对浏览器和Node.js都成立。这里的单线程的确切意思是JS的执行只在单一线程上,但Node.js 和 浏览器本身都是多线程甚至多进程的。

Q2: 我看了下https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/的最新文档似乎有更新,我从头再看一遍来给一个确切的回答。

关于Q2,最新的文档似乎跟我原来看的略有不同,根据最新的文档,有些措辞需要更新,我会更新上面的相关部分,并在这里重点讲一下 pending(I/O) callbacks phase vs poll phase

大部分的I/O回调会在poll阶段被执行,但某些系统操作(比如TCP类型错误)执行回调会安排在pending callbacks阶段。

更多细节在http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop

rueian commented 6 years ago

順帶一提 Node.js 10 開始有可以利用多線程的 Worker Thread 功能 https://nodejs.org/api/worker_threads.html

LiangRenDev commented 6 years ago

@creeperyang 非常感謝及時回复,那么关于Q2我还有一個问题,为什么系统操作的回調不会在poll阶段來执行呢?我的理解是Poll阶段只执行用户定义和Node库定义的回調,也就是比較高層的回調,而把底層的回調放在Pending Callbacks階段執行,那麽這麽做有什麽好處呢?是不是爲了優先回復Success的用戶request,而把failure的response放到後面來執行從而提高用戶的體驗?這是我的理解。

creeperyang commented 6 years ago

@LeonAppDev 更准确的说,pending callbacks阶段执行的是一些被有意延迟的回调。

详情可以参考Node.js更新相应文档的原因:

Only some callbacks that were deliberately delayed will be executed in this phase (hence "pending").

下面讲一讲ECONNREFUSED的回调(处理错误)被延迟到pending callbaks的可能原因。

int uv__tcp_connect(uv_connect_t* req,
                    uv_tcp_t* handle,
                    const struct sockaddr* addr,
                    unsigned int addrlen,
                    uv_connect_cb cb) {
  int err;
  int r;

  assert(handle->type == UV_TCP);

  if (handle->connect_req != NULL)
    return -EALREADY;  /* FIXME(bnoordhuis) -EINVAL or maybe -EBUSY. */

  err = maybe_new_socket(handle,
                         addr->sa_family,
                         UV_STREAM_READABLE | UV_STREAM_WRITABLE);
  if (err)
    return err;

  handle->delayed_error = 0;

  do {
    errno = 0;
    r = connect(uv__stream_fd(handle), addr, addrlen);
  } while (r == -1 && errno == EINTR);

  /* We not only check the return value, but also check the errno != 0.
   * Because in rare cases connect() will return -1 but the errno
   * is 0 (for example, on Android 4.3, OnePlus phone A0001_12_150227)
   * and actually the tcp three-way handshake is completed.
   */
  if (r == -1 && errno != 0) {
    if (errno == EINPROGRESS)
      ; /* not an error */
    else if (errno == ECONNREFUSED)
    /* If we get a ECONNREFUSED wait until the next tick to report the
     * error. Solaris wants to report immediately--other unixes want to
     * wait.
     */
      handle->delayed_error = -errno;
    else
      return -errno;
  }

  uv__req_init(handle->loop, req, UV_CONNECT);
  req->cb = cb;
  req->handle = (uv_stream_t*) handle;
  QUEUE_INIT(&req->queue);
  handle->connect_req = req;

  uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);

  if (handle->delayed_error)
    uv__io_feed(handle->loop, &handle->io_watcher);

  return 0;
}

从libuv源码可以看到,对ECONNREFUSED错误,Solaris系统会立即报告错误,而其它unixes操作系统则会等待。所以,统一塞到pending queue里面,延迟报告错误是一个很自然的选择——屏蔽系统差异,保持一致性。

coolpail commented 5 years ago

赞,关注一波

surahe commented 5 years ago

问题1:

看到有回复得出readFile的回调在 I/O callbacks 触发,按照这样的话,我觉得流程是这样:

但这样的话完全不符合实际结果,到底是什么原因?

const fs = require('fs')

fs.readFile(__filename, () => {
  console.log('readFile')
  setTimeout(() => {
    console.log('readFile  timeout')
  }, 0)
  setImmediate(() => {
    console.log('readFile  immediate')
  })
  fs.readFile(__filename, () => {
    console.log('readFile  readFile')
  })
})

setImmediate(() => {
  console.log('immediate')
})

setTimeout(() => {
  console.log('timeout')
}, 0)

结果是

immediate
timeout
readFile
readFile  immediate
readFile  timeout
readFile  readFile

问题2: 下面的代码不在主模块调用,也不在I/O操作中调用,为什么结果会不一样?

setImmediate里面嵌套

setImmediate(() => {
  console.log('setImmediate')
  setTimeout(() => {
    console.log('setImmediate 里面的 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('setImmediate 里面的 setImmediate')
  })
});

// 结果
// setImmediate
// setImmediate 里面的 setTimeout
// setImmediate 里面的 setImmediate
// 或
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout

setTimeout里面嵌套

setTimeout(() => {
  console.log('setImmediate')
  setTimeout(() => {
    console.log('setImmediate 里面的 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('setImmediate 里面的 setImmediate')
  })
}, 0);

// 结果
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout
uioz commented 5 years ago

@surahe 刚好发现一篇文章, 解释的很清楚 https://segmentfault.com/a/1190000013102056#articleHeader9