Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libuv源码粗读(6):libuv event-loop详解 #35

Open
xtx1130 opened this issue Mar 11, 2019 · 0 comments
Open

libuv源码粗读(6):libuv event-loop详解 #35

xtx1130 opened this issue Mar 11, 2019 · 0 comments

Comments

@xtx1130
Copy link
Owner

xtx1130 commented Mar 11, 2019

在前面五篇关于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中的回调。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant