在前面五篇关于libuv的文章中,一一把event-loop中涉及到的句柄做了简单的介绍,这篇文章我们来详细解读一下event-loop
libuv event-loop简介
event-loop
相关的代码直接翻到core.c,这里面的逻辑非常清晰。
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); // 如果不存在直接更新event-loop的loop->time(libuv事件循环内部维护的时间)
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop); // 更新`loop->time`的时间
uv__run_timers(loop); // 处理timers相关事件
ran_pending = uv__run_pending(loop); // 处理pending相关事件
uv__run_idle(loop); // 处理idle相关事件
uv__run_prepare(loop); // 处理prepare相关事件
timeout = 0; // 初始化uv__io_poll的轮询时间timeout
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) // 添加对evnet-loop运行模式的判断,从而决定uv__io_poll要阻塞的时长
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout); // 执行`uv__io_poll`阻塞循环`timeout`时长
uv__run_check(loop); // 处理check相关事件
uv__run_closing_handles(loop); // 处理close相关事件
if (mode == UV_RUN_ONCE) { // 添加对evnet-loop运行模式的判断,从而决定是否再次更新loop->time处理timers相关事件
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop); // 判断是否还存在活跃句柄
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) // 添加对evnet-loop运行模式的判断从而决定是否跳出event-loop
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
uv_run
是evnet-loop的核心方法,其中设定了事件循环中关键的触发逻辑,通读一下这段代码就能得出来初步的认识,相关注释已经加入到了源码的后面。
事件循环中的几个判断
在event-loop的代码中,大家可以发现其中掺杂了一些判断语句,这一章节给大家详细解释一下相关的判断流程。
uv_run_mode简介
在介绍这里面的判断之前,先详细介绍一下uv_run_mode
,其取值有三种,分别为:
UV_RUN_DEFAULT
默认轮询模式,此模式会一直运行事件循环直到没有活跃句柄、引用句柄、和请求句柄UV_RUN_ONCE
一次轮询模式,此模式如果pending_queue
中有回调,则会执行回调而直接跨过uv__io_poll
。如果没有,则此方式只会执行一次i/o轮询(uv__io_poll
)。如果在执行过后有回调压入到了pending_queue
中,则uv_run
会返回非0,你需要在未来的某个时间再次触发一次uv_run
来清空pending_queue
。UV_RUN_NOWAIT
一次轮询(无视pending_queue
)模式,此模式类似UV_RUN_ONCE
但是不会判断pending_queue
是否存在回调,直接进行一次i/o轮询。
活跃句柄判断
活跃句柄判断代码如下:
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
这个地方主要是用于判断本次事件循环中是否有活跃句柄,uv__loop_alive
方法展开如下:
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_loop_t
)中是否还存在活跃句柄(loop->active_handles
)和请求句柄(loop->active_reqs.count),其次,对未结束的句柄进行了判断如果存在未结束的句柄会在后面的uv__run_closing_handles(loop)
进行句柄的unref操作,之后会调用handle->close_cb(handle);
来触发执行close事件回调。
timeout赋值之前的判断
代码如下:
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
timeout
变量决定了uv__io_poll
的阻塞时长,如果大家翻看过我之前写过的文章,在node源码粗读(8):setImmediate注册+触发全流程解析的uv_idle简介
章节中,详细介绍了timeout
部分,在这里我就不做过多讲解了。
额好吧……在这里再多说一句,!ran_pending
会进入到判断中是为了验证其余条件是否满足跳过poll阶段,而如果pending_queue
存在的话是可以直接跨过poll阶段的没有必要进入到uv_backend_timeout
中做多余的判断,这里是结合了mode == UV_RUN_ONCE && !ran_pending
所作出的判断。
UV_RUN_ONCE模式判断
代码如下:
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
uv__run_timers(loop);
}
这个地方是对UV_RUN_ONCE
追加的保证uv__io_poll
阻塞之后定时器到期所进行的回调。而UV_RUN_NOWAIT
则是单纯的为了进行一次i/o轮询,目的性强不保证进度,因此在检查中省略了它。
libuv loop->time 时间计算详解
在代码中,大家可以发现,uv__update_time
总是伴随着uv__run_timers
出现。下面给大家解释下uv__update_time
:
UV_UNUSED(static void uv__update_time(uv_loop_t* loop)) {
/* Use a fast time source if available. We only need millisecond precision.
*/
loop->time = uv__hrtime(UV_CLOCK_FAST) / 1000000;
}
在libuv的uv_loop_t
结构体中会维护一个time
属性,这个loop->time
则是event-loop中用来执行定时任务的时间计算器,每次调用他都会更新出最新的event-loop时间,这个时间则是和uv_timer_t
息息相关的,uv_timer_t
注册代码如下:
int uv_timer_start(uv_timer_t* handle,
uv_timer_cb cb,
uint64_t timeout,
uint64_t repeat) {
uint64_t clamped_timeout;
if (cb == NULL)
return UV_EINVAL;
if (uv__is_active(handle))
uv_timer_stop(handle);
clamped_timeout = handle->loop->time + timeout;
if (clamped_timeout < timeout)
clamped_timeout = (uint64_t) -1;
handle->timer_cb = cb;
handle->timeout = clamped_timeout;
handle->repeat = repeat;
/* start_id is the second index to be compared in uv__timer_cmp() */
handle->start_id = handle->loop->timer_counter++;
heap_insert(timer_heap(handle->loop),
(struct heap_node*) &handle->heap_node,
timer_less_than);
uv__handle_start(handle);
return 0;
}
通过clamped_timeout = handle->loop->time + timeout;
这段代码可以发现,uv__run_timers
真正的运行时间是loop->time
(uv_timer_t
句柄注册时的event-loop时间)+ timeout
(延迟触发时间)。
setTimeout和setImmediate
在nodejs中,如果你输入如下代码:
setTimeout(()=>console.log(0))
setImmediate(()=>console.log(1))
会发现输出顺序是随机的,接下来给大家详细解释一下这里的随机性,视线首先转移到setTimeout
的实现原理[internal/timers.js]中:
function Timeout(callback, after, args, isRepeat) {
after *= 1; // coalesce to number or NaN
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
// ...
after = 1; // schedule on next tick, follows browser behavior
}
// ...
}
在这里对setTimeout
的延迟时间做了判定,如果没有设定延迟时间则会默认为1毫秒的延迟触发。继而延伸到libuv,在event-loop的uv__run_timers
中调用handle->timer_cb(handle)
来触发回调。在node源码粗读(8):setImmediate注册+触发全流程解析的setImmediate的执行
章节中,详细介绍了setImmediate
的执行机制,setImmediate
是在uv__run_check
阶段触发。
在libuv进行初始化的过程中,如果时间小于1毫秒,则会直接跳过uv__run_timers
使得uv__run_check
中的回调队列优先触发;而如果初始化时间大于1毫秒,则会进入到uv__run_timers
阶段优先触发setTimeout
中的回调。
原文地址:https://github.com/xtx1130/blog/issues/35,如果其中内容有误,欢迎大神斧正