libuv源码粗读(6):libuv event-loop详解
发布于 7 年前 作者 xtx1130 4784 次浏览 来自 分享

在前面五篇关于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,如果其中内容有误,欢迎大神斧正

回到顶部

AltStyle によって変換されたページ (->オリジナル) /