描述
引用 nodejs.org
的描述
When called, the active Timeout object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the Timeout object’s callback is invoked. Calling timeout.unref() multiple times will have no effect.
简单的可以理解为:timer.unref
标记句柄,当进程中再无存活的事件,此时的 timer
句柄不会阻止进程退出。
示例
先看看没有使用 unref
标记的定时器实现:
1 | console.log('a'); |
执行上面代码,依次打印 a, b
后等待 3s
左右,打印 c
之后没有新的事件进程才会退出。使用 unref
标记后:
1 | console.log('a'); |
依次打印 a, b
后进程直接退出,timeout
句柄并没有阻止进程的退出。
引用计数
上面的例子 timeout
回调函数其实是已经推进了事件循环当中,不过为什么不会等待事件循环执行结束才退出进程呢?这时候引入了一个 引用计数
的概念,可以简单理解为当一个新的事件加入事件循环的队列中时,引用计数
就会 +1
,当执行完事件后,引用计数
就会 -1
,直到没有新的事件进来,引用计数
等于 0
的时候进程退出。unref
调用后,引用计数
会 -1
。
使用上面的规则对 示例
里的例子进行解析:
当执行到 setTimeout
时,将回调函数推进事件循环,引用计数 +1
,执行 timeout.unref()
时 引用计数 -1
, 引用计数
为 0
,后面的代码也没有新的事件加入循环,执行文件末尾后进程退出。
node
包装层
基于 Node.js v14.0.0-pre 版本
在 node.js
里,setTimeout
函数被挂载到运行时上下文中的 global
对象上,具体的实现代码在 node/lib/timer.js
中:
1 |
|
可以看到执行 setTimeout
是返回了 Timeout
的实例,Timeout
里的实现如下:
1 |
|
实例化 Timeout
时标记引用,全局的引用计数 + 1,并调用 c++ 层函数 toggleTimerRef
传值 true,再看看 c++ 层的封装:
1 | // src/timer.cc |
结合两个文件, Environment
类调用内联函数 GetCurrent
返回 Environment
实例,最终调用到 env.cc
文件里的 ToggleTimerRef
函数:
1 |
|
最终的逻辑走到了 libuv
的两个函数 uv_ref
和 uv_unref
上。
libuv
实现层
基于Libuv v1.35.0 版本
函数实现在 libuv/src/uv-common.c
,逻辑比较简单的两个函数:
1 | // uv-common.c |
uv_ref
和 uv_unref
函数指向了头文件定义的两个宏函数,当句柄仍在活动状态时,标记引用或取消引用标记,并在事件循环的全局活动句柄计数器上 +1
或者 -1
。
运行时
上面描述了初始化 Timeout
实例和使用 unref
函数 最终会在 libuv
层将事件循环的全局句柄活动计数器 active_handles
上 ±1
。这个计数器是如何个用途,回到 node
层,当我们执行 node xxx.js
时最开始走到了 node_main.cc
文件里的 main
函数,流程走到了 node_main_instance.cc
里:
1 |
|
我删减了一些代码,这样看得主逻辑更清晰一点,while
里检查事件循环是否还有存活的句柄,如果没有则退出循环、退出进程。这里有使用了 libuv
的函数
uv_run
和 uv_loop_alive
,看看逻辑实现:
1 | // libuv/src/unix/core.c |
在 uv_run
函数里就 while
是大家都听过无数遍的事件循环, 其中在第一步的 timers
阶段,执行到期的定时器的同时,还把上面 ref
标记的全局句柄活动计数器 active_handles
进行 -1
操作。流程走到这里就清晰了,再回到最初的地方:
- 没有使用
unref
:
1 | console.log('a'); |
代码执行到 setTimeout
, 回调函数进入事件循环 active_handles + 1 = 1
,因为代码没有其他的 I/O
事件,事件循环一直循环定时期定时的时间到了后,执行 timers
阶段的事务,执行回调函数,并对活动句柄计数器减一操作 active_handles - 1 = 0
,这时候 while (r != 0 && loop->stop_flag == 0)
条件不满足,退出循环,node
层退出循环后进程退出。
- 使用
unref
:
1 | console.log('a'); |
代码执行到 setTimeout
, 回调函数进入事件循环 active_handles + 1 = 1
,随后执行 unref
操作 active_handles - 1 = 0
,事件循环条件 while (r != 0 && loop->stop_flag == 0)
条件不满足,退出循环,node
层退出循环后进程退出。
最后以一层流程图结束本文:
如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理