nodejs Timer

  • nodejs Timer

    • timer.unref()的失效情况

      • 先看timer.unref的底层调用
      • 对失效的解释
    • 定时器的创建
      • TimerWrap

        • TimerWrap()
        • 再重点看下TimerWrap里的uv_timer_init
        • 总结
    • setInterval定时器
    • clearTimeout, clearInterval
    • setImmediate
    • node timer触发
      • uv_run_timers
      • 事件循环结束
    • Timer总结

本文章最好配合nodejs c语言源码一起,下面几个链接是相关的写的非常好的文章。

github优化资料
alinode源码
alinode TIme
nodejs timer解析

timer.unref()的失效情况

  • 有效用法
    下面的timer1在创建后被unref, 所以不会执行callback函数;
let timer1 = setInterval(()=>{console.log('timer1...');
}, 1000);
timer1.unref();
  • 失效用法
    当同时存在2个timer时, unref函数失效, 两个定时器都会执行;
let timer1 = setInterval(()=>{console.log('timer1...');
}, 1000);
let timer2 = setInterval(()=>{console.log('timer2...');
}, 2000);
timer1.unref();

先看timer.unref的底层调用

unref对应的C++代码调用:

// timer_wrap.cc
constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Timer"));
...
env->SetProtoMethod(constructor, "unref", HandleWrap::Unref);

即调用的是HandleWrapUnref方法:

// handle_wrap.cc
void HandleWrap::Unref(const FunctionCallbackInfo<Value>& args) {HandleWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());if (IsAlive(wrap))uv_unref(wrap->GetHandle());
}// uv-common.cc
void uv_unref(uv_handle_t* handle) {uv__handle_unref(handle);
}

uv-common.h里面uv__handle_unref干了两件事:
1) 取消REF位标记: (h)->flags &= ~UV__HANDLE_REF;
2) 如果timer处于active状态, 则将活跃句柄-1: loop->active_handle--;

对失效的解释

创建了2个timer后, node循环中, 有两个active_handles:

let timer1 = ...
// setInterval=>uv_timer_start(timer1) => active_handles = 1
let time2 = ...
// setInterval=>uv_timer_start(timer2) => active_handles = 2

active_handles>0激活loop, 这里L1为循环为标记点:

// L1: ative_handles > 0 => loop()

由于unref只是标记了REF, 这里还是执行了timeout (TODO需要说明为什么还会执行??), 当timer1到时后, 判断了active_handles=1, loop仍然处于acitve, 所以会执行再次启动定时器uv_timer_start(timer1):

// timer1 timeout => uv_timer_stop(timer1) active_handles = 1  => callback() => uv_timer_start(timer1) active_handles = 2
// timer2 timeout => uv_timer_stop(timer2) active_handles = 1  => callback() => uv_timer_start(timer2) active_handles = 2
// goto L1

也就是说, 就是看loop是否active状态, 处于active, 则会执行循环中的handle, loop是否在activeactive_handles是否大于0;

#define uv__has_active_handles(loop)                                          \((loop)->active_handles > 0)

??? 具体为什么在unref后还会执行, 需要查验, REF标记对handle执行的影响

定时器的创建

setTimeout为入口,
1. new Timeout();
这里的Timeout对象timer时挂在TimerList链表上的node, 所以其结构如下, 其中的_idlePrev, _idleNext为链表指针:

function Timeout(after, callback, args) {this._called = false;this._idleTimeout = after;this._idlePrev = this;this._idleNext = this;this._idleStart = null;this._onTimeout = callback;this._timerArgs = args;this._repeat = null;
}
  1. active(timer)

    • 激活定时器就是将其挂在TimerList上, 调用list的insert(timer, false)函数.
    • 其实现可以看到, 这里的定时器分了两种, 分别为链表集:unrefedListsrefedLists, setTimeoutunrefed=false, 属于unrefedLists, 这个ref具体什么意识,目前还不是很清楚;
function insert(item, unrefed) {const msecs = item._idleTimeout;...item._idleStart = TimerWrap.now();const lists = unrefed === true ? unrefedLists : refedLists;var list = lists[msecs];if (!list) {lists[msecs] = list = createTimersList(msecs, unrefed);  // new TimeWrap()}L.append(list, item);
}
  1. new TimeWrap()创建C定时器
    createTimersList()时, 整个TimerList公用一个定时器, 返回给了_timer, 且指定了当前的list的超时时间:
function TimersList(msecs, unrefed) {...this._timer = new TimerWrap();...this.msecs = msecs;
}
  1. L.append()将当前定时器挂在链表上
    参考linkedlist.js的实现, L.append()是在链表末尾挂载定时器(相同timeout, 后挂载的超时事件越晚);

  2. 分析TimersList的创建
    下面初始化链表创建了一个TimerWrap对象, 后面的源码分析说明, 这里的_timer是一个C定时器对象

// createTimersList()
const list = new TimersList(msecs, unrefed);
// TimersList()this._timer = new TimerWrap();
  1. unref
    这里对引用参数进行了判断, 其底层调用了uv_unref, 如果为true, 则将该定时器标记为非REF
// createTimersList()
if (unrefed === true) list._timer.unref();
  1. 定时器启动list._timer.start(msecs)
    调用TimerWrap::Start()函数,
// TimerWrap::Start()
int err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, 0);

uv_timer_start是与平台相关, 看unix实现, 下面的代码可见, unix下的定时器是以二叉树最小堆的结构来保存的, 相关原因看本页前面的几个链接.

int uv_timer_start(uv_timer_t* handle,uv_timer_cb cb,uint64_t timeout,uint64_t repeat) {...handle->start_id = handle->loop->timer_counter++;heap_insert((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_start(handle);...
}
// uv__handle_start()
#define uv__handle_start(h) \
...
(h)->flags |= UV__HANDLE_ACTIVE;   \   // activing
if (((h)->flags & UV__HANDLE_REF) != 0) uv__active_handle_add(h);  \      // refed => add()
...

heap_insrt函数定位于heap_inl.h头文件中, 实现的是一个二叉树结构的最小堆, 查找与插入复杂度均为O(logn).

  1. 定时器链表超时检查与调用回调
    下面的listOnTimeout为封装的对该TimersList的定时器的超时调用,
// createTimersList()list._timer[kOnTimeout] = listOnTimeout;// listOnTimeout()
....
var now = TimerWrap.now();
...while (timer = L.peek(list)) {diff = now - timer._idleStart;if (diff < msecs) {...// 剩余时间发生变化, 跟新当前定时器在定时器最小堆中的位置, 只要判断链表头的Timeout对象, 所以直接退出this.start(timeRemaining);return;}// 定时器超时// 链表中清除L.remove(timer);if (!timer._onTimeout) continue;...// 执行回调tryOnTimeout(timer, list);...}// 链表为空, 做清除工作assert(L.isEmpty(list));this.close();if (list._unrefed === true && list === unrefedLists[msecs]) {delete unrefedLists[msecs];} else if (list === refedLists[msecs]) {delete refedLists[msecs];}
  1. 定时器链表的清除工作
    上面的代码给出了定时器链表为空时做的清除工作:
    1) 关闭定时器对象this.close()
    该函数调用的是HandleWrap::Close(), 其中主要调用了uv_close(wrap->handle_, OnClose);, 而这个函数也是与平台相关, unix底层实现就是从最小堆中删除该定时器:
switch (handle->type) {....case UV_TIMER:uv__timer_close((uv_timer_t*)handle);break;....
}// timer.c
void uv__timer_close(uv_timer_t* handle) {uv_timer_stop(handle);
}
int uv_timer_stop(uv_timer_t* handle) {....// 从定时器堆中删除heap_remove((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_stop(handle);    // 标记为!_ACTIVE, 删引用REFreturn 0;
}

2) 释放链表的内存delete ...

TimerWrap

TimerWrap()

看看TimerWrap干了些什么, 下面的构造函数里调用了父类HandlerWrap的构造函数, 然后执行了初始化uv_timer_init:

// timer_wrap.ccTimerWrap(Environment* env, Local<Object> object): HandleWrap(env,object,reinterpret_cast<uv_handle_t*>(&handle_),AsyncWrap::PROVIDER_TIMERWRAP) {int r = uv_timer_init(env->event_loop(), &handle_);CHECK_EQ(r, 0);}

上面的HandleWrap做了一些v8环境与句柄的初始化(还没看懂), 这里的handle_是一个uv通用结构, 封装了unix和windows, 用户完成具体的事件循环:

uv_timer_t handle_;

其又包含(继承)通用结构, 该结构中有dataloop对象:

#define UV_HANDLE_FIELDS            \/* public */                      \void* data;                       \/* read-only */                   \uv_loop_t* loop;                  \uv_handle_type type;              \ ...

loop对象的结构如下, 具体的结构与平台的回调库相关, 这里我们注意两个公共的数据: data(用户定义的数据, 即后面的handle_->data = this;)与active_handlers(消息循环引用计数器),

struct uv_loop_s {/* User data - use this for whatever. */void* data;/* Loop reference counting. */unsigned int active_handles;...
};

具体的东西很底层, 没看;

再重点看下TimerWrap里的uv_timer_init

这里的handler_可以理解为定时器句柄.
uv_timer_init具体实现与平台相关,

// timer_warp.cc TimerWrap构造函数
int r = uv_timer_init(env->event_loop(), &handle_);
CHECK_EQ(r, 0);

看下unix的实现

// uv/src/unix/timer.c
int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) {uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER);handle->timer_cb = NULL;handle->repeat = 0;return 0;
}

调用了通过的handle_init, 主要就是执行了写初始化, 并标记为REF状态.

#define uv__handle_init(loop_, h, type_)             \do {                                               \(h)->loop = (loop_);                             \(h)->type = (type_);                             \(h)->flags = UV__HANDLE_REF;  /* Ref the loop when active. */       \   ....}                                                  \while (0)

显然, setTimeout函数的unrefed=false, 所以属于引用类型, node内部使用的_unrefActive()属于unrefed=true;


总结

TimerWrap就是构造了一个平台相关的C定时器.

setInterval定时器

  1. 其实现与setTimeout一样, 只是创建Timeout对象后, 设置了timer._repeat = repeat;
  2. 在执行回调函数后, 判断了重复
// ontimeout
if (timer._repeat)rearm(timer);

rearm实现里, 就是重写开始计时, 这里的_handle可能不是Timeout实例(???_unrefAcitve()??)

// rearm
function rearm(timer) {// // Do not re-arm unenroll'd or closed timers.if (timer._idleTimeout === -1) return;// If timer is unref'd (or was - it's permanently removed from the list.)if (timer._handle && timer instanceof Timeout) {timer._handle.start(timer._repeat);} else {timer._idleTimeout = timer._repeat;active(timer);}
}

clearTimeout, clearInterval

clearTimeout实现如下, clearInterval时对clearTimeout的封装, 仅仅多了置_repeat=null, 下面的unenroll好像时内部对就重复利用的定时器, 现在还没看到:

exports.clearTimeout = function(timer) {if (timer && (timer[kOnTimeout] || timer._onTimeout)) {timer[kOnTimeout] = timer._onTimeout = null;if (timer instanceof Timeout) {timer.close(); // for after === 0} else {unenroll(timer);    // ????????}}
};

setImmediate

  • 全局只有条立即执行队列
    该队列为一单链表, 链接的节点是new Immediate()对象
// Create a single linked list instance only once at startup
var immediateQueue = new ImmediateList();function ImmediateList() {this.head = null;this.tail = null;
}
  • setImmediate(cb)函数做了3件事:

    1. 实例化Immediate对象,并初始化;
    2. 将对象挂载到全局的立即执行回调队列immediateQueue;
    3. 同时, 对全局process对象挂载立即执行回调函数_immediateCallback
function createImmediate(args, callback) {var immediate = new Immediate();immediate._callback = callback;immediate._argv = args;immediate._onImmediate = callback;if (!process._needImmediateCallback) {process._needImmediateCallback = true;process._immediateCallback = processImmediate;}immediateQueue.append(immediate);return immediate;
}
  • 全局process._immediateCallback调用
function processImmediate() {var immediate = immediateQueue.head;var tail = immediateQueue.tail;var domain;// 立即执行链表只会调用一次, 所以,这里提前删除, 以防在callback执行期间又创建了一个immediateQueue.head = immediateQueue.tail = null;// 遍历执行链表while (immediate) {...immediate._callback = immediate._onImmediate;// Save next in case `clearImmediate(immediate)` is called from callbackvar next = immediate._idleNext;tryOnImmediate(immediate, tail);....// If `clearImmediate(immediate)` wasn't called from the callback, use the `immediate`'s next item  这里的判断好像用处不大if (immediate._idleNext)immediate = immediate._idleNext;elseimmediate = next;}...  //后面这几句没看懂
}
  • Immediate回调函数执行
    回调函数实在try{}catch{}下执行, 如果抛出异常, 仍然会在下一帧继续执行后面的回调函数:
function tryOnImmediate(immediate, oldTail) {var threw = true;try {runCallback(immediate);threw = false;} finally {if (threw && immediate._idleNext) {// Handle any remaining on next tick, assuming we're still alive to do so....const next = immediate._idleNext;if (curHead) {...} else {immediateQueue.head = next;immediateQueue.tail = oldTail;}process.nextTick(processImmediate);}}
}

node timer触发

在回调主循环uv_run里面, 每帧一开始就会处理定时器:

// unix.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {while (r != 0 && loop->stop_flag == 0) {uv__update_time(loop);uv__run_timers(loop);....r = uv__loop_alive(loop);}
}

uv_run_timers

uv_run_timers的实现如下, 就是不断从最小堆中去除根节点, 判断是否超时, 如果没超时则退出:

void uv__run_timers(uv_loop_t* loop) {struct heap_node* heap_node;uv_timer_t* handle;for (;;) {heap_node = heap_min((struct heap*) &loop->timer_heap);if (heap_node == NULL)break;handle = container_of(heap_node, uv_timer_t, heap_node);if (handle->timeout > loop->time)break;uv_timer_stop(handle);uv_timer_again(handle);handle->timer_cb(handle);}
}

而如果超时, 则上面调用了uv_timer_stop来停止定时器与uv_timer_again来从启定时器.
下面时这两个函数的实现:
* uv_timer_stop
1) 取得定时器, 从最小堆中移除;
2) 标记~ACTIVE, 并减计数器;

// uv/src/unix/timer.c
int uv_timer_stop(uv_timer_t* handle) {if (!uv__is_active(handle))return 0;heap_remove((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_stop(handle);return 0;
}
  • uv_timer_again
    如果定时器要求重复触发(repeat), 则先停止, 再从新启动(跟新超时时间, 插入heap)
int uv_timer_again(uv_timer_t* handle) {if (handle->timer_cb == NULL)return -EINVAL;if (handle->repeat) {// 先移除uv_timer_stop(handle);// 再从新启动uv_timer_start(handle, handle->timer_cb, handle->repeat, handle->repeat);}return 0;
}

事件循环结束

  • 上面uv_run里面, 循环继续的条件是uv__loop_alive(loop) != 0 && loop->stop_flag == 0, 即要么loop被标记为STOP状态, 要么uv__loop_alive返回为0;
  • uv__loop_alive的实现又说明, 在loop里的活跃句柄active_handles对象数只要大于0就不会退出循环, 这就是本文前面的例子里解释;

前面的例子定义了2个setInterval对象, unref()了一个, 此时loop->active_handles=1, 所以循环会继续, 在执行uv__run_timers时依次执行uv_timer_stopuv_timer_again, 由于两个定时器都有repeat==true, 所以两个定时器会再次执行uv_timer_start, 此时没有被unref的定时器再一次会执行uv__active_handle_add(), 所以循环会一直持续下去;

// uv/src/unix/timer.c
static int uv__loop_alive(const uv_loop_t* loop) {return uv__has_active_handles(loop) ||uv__has_active_reqs(loop) ||loop->closing_handles != NULL;
}
// uv-common.h
#define uv__has_active_handles(loop)   \((loop)->active_handles > 0)

总结
unref仅仅时将定时器对象句柄解引用, 不是将定时器从定时器链表中移除, 所以如果循环没有结束, 则定时器还是会被调用;

Timer总结

  1. let timer = setTimeout(cb, t);函数返回的不是一个真的定时器对象, 而是一个js对象Timeout, 该对象是TimersList链表的节点类型, 这里我称它为虚定时器;
  2. TimersList身上的_timer才是真正的C定时器对象, 每个list上的所有虚定时器共享这个_timer, 同一个链表上的所有虚定时器有相同的超时时间;
  3. 链表本身是由list节点链接首尾节点组成的闭合链表, 检查超时每次只要取头部虚定时器, 添加时只要在尾部添加, 时间复杂度都为O(1);
  4. unix定时器对象在底层的存储方式为最小堆结构, 增删查定时器复杂度O(logn);

nodejs定时器setInterval,setTimeout,clearTimeout, clearInterval源码学习相关推荐

  1. 【iScroll源码学习01】准备阶段 - 叶小钗

    [iScroll源码学习01]准备阶段 - 叶小钗 时间 2013-12-29 18:41:00 博客园-原创精华区 原文  http://www.cnblogs.com/yexiaochai/p/3 ...

  2. Electron源码学习: Electron组成与初始化流程

    Electron源码学习: Electron组成与结构 前言 ​ 最近研究学习Electron的源码结构已经有一些小的进展, 越接触Electron就越发现组成这个软件的大集合不得了.现在学习到的仍然 ...

  3. Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)

    在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...

  4. 博通Broadcom SDK源码学习与开发5——ECOS系统层剖析

    声明:原创作品,严禁用于商业目的. 本系列文章将全面剖析以Bcm33xxx芯片开发Cablemodem产品的SDK源码为例,从编译系统到各个功能模块进行分析与探讨. 文章目录 0.写在前篇 1. Op ...

  5. 源码学习-net/http

    package net/http是Go语言的主要应用场景之一web应用的基础,从中可以学习到大量前文提到的io,以及没有提到的sync包等一系列基础包的知识,代码量也相对较多,是一个源码学习的宝库.本 ...

  6. Vuex 4源码学习笔记 - Vuex是怎么与Vue结合?(三)

    在上一篇笔记中:Vuex源码学习笔记 - Vuex开发运行流程(二) 我们通过运行npm run dev命令来启动webpack,来开发Vuex,并在Vuex的createStore函数中添加了第一个 ...

  7. Electron源码学习:Electron加密与安全

    Electron加密与安全 引言 ​ 目前网络上主要流传的加密就只是网页文件打包成asar和JS混淆加密,以及用addon的方式,这几种方式的话,基本就没有什么破解难度.针对的官方asar的打包,这种 ...

  8. 【Android 源码学习】SystemServer启动原理

    Android 源码学习 SystemServer启动原理 望舒课堂 SystemServer进程启动原理学习记录整理. 参考文章: Android系统启动流程(三)解析SyetemServer进程启 ...

  9. Shiro源码学习之二

    接上一篇 Shiro源码学习之一 3.subject.login 进入login public void login(AuthenticationToken token) throws Authent ...

最新文章

  1. JDBC+Servlet+JSP整合开发之30-JDBC、Servlet、JSP的MVC
  2. [Apache] Apache 從 2.2 換至 2.4 httpd.conf 的調整筆記 (windows 環境)
  3. SQL 2005各版本的区别
  4. Java queue总结
  5. 数据可视化|实验五 分析1996-2015年人口数据各个特征的分布与分散状况
  6. NSArray创建和使用
  7. linux内核编译及添加系统调用(hdu)_浅谈关于Linux内核write系统调用操作的原子性
  8. Kotlin学习笔记 第二章 类与对象 第十四 十五节 委托 委托属性
  9. ssd颗粒查看工具_贴吧机佬强烈推荐的游戏SSD?西数蓝盘3D M.2 500G实测
  10. Sicily/1927. Conflict
  11. 自己碰到的一个“无法读取源文件或磁盘”问题处理
  12. 移动开发用户行为分析神器之--AppSee!
  13. 企业管理系统可视化权限功能设计
  14. CST启用GPU加速的调试笔记
  15. Function类型(函数)
  16. BTC源码分析 交易(一)
  17. 喵星球上的点名(后缀自动机+dfs序+莫队)
  18. ie浏览器下载文件中文名称乱码
  19. java-net-php-python-jspm零担快跑物流管理系统计算机毕业设计程序
  20. 9年级计算机主要学的什么好,九年级信息技术下册教学计划

热门文章

  1. 树莓派+GPS之调试测试记录篇
  2. ACM暑假总结7.31
  3. [转载] 全本张广泰——第六回 大爷起歹心 白犬换广泰
  4. route 命令详解
  5. 位图文件大小的精准计算方法
  6. 安徽省计算机专业专科排名2015,2016年安徽专科学校排名汇总
  7. 一种网络模式切换延时场景介绍
  8. [Zookeeper基础]-- linux下搭建zookeeper集群
  9. Transformer是什么?看完这篇你就醍醐灌顶
  10. 2014年4月25日_欢迎新作者:2014年3月/ 4月