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

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

回到顶部