nodejs 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);
即调用的是HandleWrap
的Unref
方法:
// 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
是否在active
看active_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;
}
active(timer)
- 激活定时器就是将其挂在
TimerList
上, 调用list的insert(timer, false)
函数. - 其实现可以看到, 这里的定时器分了两种, 分别为链表集:
unrefedLists
与refedLists
,setTimeout
的unrefed=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);
}
new TimeWrap()
创建C定时器
createTimersList()
时, 整个TimerList
公用一个定时器, 返回给了_timer
, 且指定了当前的list的超时时间:
function TimersList(msecs, unrefed) {
...
this._timer = new TimerWrap();
...
this.msecs = msecs;
}
L.append()
将当前定时器挂在链表上
参考linkedlist.js
的实现,L.append()
是在链表末尾挂载定时器(相同timeout, 后挂载的超时事件越晚);分析
TimersList
的创建
下面初始化链表创建了一个TimerWrap
对象, 后面的源码分析说明, 这里的_timer
是一个C定时器对象
// createTimersList()
const list = new TimersList(msecs, unrefed);
// TimersList()
this._timer = new TimerWrap();
unref
这里对引用参数进行了判断, 其底层调用了uv_unref
, 如果为true
, 则将该定时器标记为非REF
// createTimersList()
if (unrefed === true) list._timer.unref();
- 定时器启动
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)
.
- 定时器链表超时检查与调用回调
下面的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) 关闭定时器对象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, 删引用REF
return 0;
}
2) 释放链表的内存delete ...
TimerWrap
TimerWrap()
看看TimerWrap
干了些什么, 下面的构造函数里调用了父类HandlerWrap
的构造函数, 然后执行了初始化uv_timer_init
:
// timer_wrap.cc
TimerWrap(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_;
其又包含(继承)通用结构, 该结构中有data
与loop
对象:
#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定时器
- 其实现与
setTimeout
一样, 只是创建Timeout
对象后, 设置了timer._repeat = repeat;
- 在执行回调函数后, 判断了重复
// 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件事:
- 实例化
Immediate
对象,并初始化; - 将对象挂载到全局的立即执行回调队列
immediateQueue
; - 同时, 对全局
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 callback
var 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;
else
immediate = 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_stop
和uv_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总结
let timer = setTimeout(cb, t);
函数返回的不是一个真的定时器对象, 而是一个js对象Timeout
, 该对象是TimersList
链表的节点类型, 这里我称它为虚定时器;TimersList
身上的_timer
才是真正的C定时器对象, 每个list上的所有虚定时器共享这个_timer
, 同一个链表上的所有虚定时器有相同的超时时间;- 链表本身是由
list
节点链接首尾节点组成的闭合链表, 检查超时每次只要取头部虚定时器, 添加时只要在尾部添加, 时间复杂度都为O(1); - unix定时器对象在底层的存储方式为最小堆结构, 增删查定时器复杂度O(logn);