精华 node源码粗读(8):setImmediate注册+触发全流程解析
发布于 6 年前 作者 xtx1130 4907 次浏览 来自 分享

本篇文章主要介绍setImmediate底层的实现,主要涉及部分node和libuv源码。

前言

相信稍微对node感兴趣的同学都知道setImmediate触发是在event-loop的check阶段,那么这个setImmediate到底是在哪里实现的注册到uv_check中以及如何触发其中的回调呢?

js入口

如果要想撕开setImmediate的口子,那么最简单粗暴的方式就是直接从./lib/timers这里入手。闲话少说,直接撸代码:

// lib/timers.js setImmediate回调追踪
// ...
const Immediate = class Immediate {
  constructor(callback, args) {
    this._idleNext = null;
    this._idlePrev = null;
    // this must be set to null first to avoid function tracking
    // on the hidden class, revisit in V8 versions after 6.2
    this._onImmediate = null;
    this._onImmediate = callback; // 注意这里
    this._argv = args;
    this._destroyed = false;
    this[kRefed] = false;

    initAsyncResource(this, 'Immediate');

    this.ref();
    immediateInfo[kCount]++;

    immediateQueue.append(this);
  }
  // ...
}
// ...

这是setImmediate所调用的构造函数的一部分内容,从这里可以很明确的看出来我们注册的回调传递给了this._onImmediate
接下来我们看一下最终触发回调的代码:

// lib/timers.js setImmediate回调触发
function runCallback(timer) {
  const argv = timer._argv;
  if (typeof timer._onImmediate !== 'function')
    return promiseResolve(timer._onImmediate, argv[0]);
  if (!argv)
    return timer._onImmediate();
  Reflect.apply(timer._onImmediate, timer, argv); // timer._onImmediate通过apply触发
}

找起来也是轻松加愉快,通过_onImmediate我们很快便找到了触发函数(或者直接找apply)。这样我们可以通过函数名runCallback一层一层向上溯源了。
最终溯源的结果在这里:

// lib/timers.js setImmediate溯源
const {
  Timer: TimerWrap,
  setupTimers,
} = process.binding('timer_wrap');
const [immediateInfo, toggleImmediateRef] =
  setupTimers(processImmediate, processTimers);

向上溯源的函数是通过setupTimers进行注册的,而setupTimers定睛一看:process.binding,内置模块timer_wrap浮出了水面(在这里简单提一下,在最近的pr: timers: refactor timer list processingsetTimeout的触发函数也修改为了使用setupTimers注册)。

node中对setImmediate的处理

processImmediate函数在node中的注册

接上文,我们视线转移到./src/timer_wrap.ccSetupTimers函数中:

// src/timer_wrap.cc
// ...
  static void SetupTimers(const FunctionCallbackInfo<Value>& args) {
    CHECK(args[0]->IsFunction());
    CHECK(args[1]->IsFunction());
    auto env = Environment::GetCurrent(args);

    env->set_immediate_callback_function(args[0].As<Function>());//注意这里
    env->set_timers_callback_function(args[1].As<Function>());

    auto toggle_ref_cb = [] (const FunctionCallbackInfo<Value>& args) {
      Environment::GetCurrent(args)->ToggleImmediateRef(args[0]->IsTrue());
    };
    auto result = Array::New(env->isolate(), 2);
    result->Set(env->context(), 0,
                env->immediate_info()->fields().GetJSArray()).FromJust();
    result->Set(env->context(), 1, toggle_ref_function).FromJust();
    args.GetReturnValue().Set(result);
}

详细介绍一下env->set_immediate_callback_function(args[0].As<Function>())。这句话的来源其实在env.h中:

// src/env.h
// ...
V(immediate_callback_function, v8::Function)    
// ...
#define V(PropertyName, TypeName)                                             \
  inline v8::Local<TypeName> PropertyName() const;                            \
  inline void set_ ## PropertyName(v8::Local<TypeName> value);
  ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V)
#undef V

整体意思是:定义了一个带参宏V,而这个宏在调用的时候会定义一个属性,所以在通过V(immediate_callback_function, v8::Function)调用后,可以实现env->set_immediate_callback_function的调用了,同时还会生成一个成员函数PropertyName(),所以在调用后同时也会使得env->immediate_callback_function成为可调用函数。
args[0]在这里指的便是processImmediate,所以通过SetupTimers可以使processImmediate这个函数最终注册到node中。

libuv中对setImmediate的处理

processImmediate函数在libuv中的注册

视线转移到src/env.cc中,不知道大家是否还记得在第一章一个简单的nodejs文件从运行到结束都发生了什么中曾经一笔带过Environment::Start。没错,setImmediate就是在Environment::Start中注册到libuv的,接下来看代码:

// src/env.cc
// ...
void Environment::Start(int argc,
                        const char* const* argv,
                        int exec_argc,
                        const char* const* exec_argv,
                        bool start_profiler_idle_notifier) {
  HandleScope handle_scope(isolate());
  Context::Scope context_scope(context());

  uv_check_init(event_loop(), immediate_check_handle());
  uv_unref(reinterpret_cast<uv_handle_t*>(immediate_check_handle()));

  uv_idle_init(event_loop(), immediate_idle_handle());

  uv_check_start(immediate_check_handle(), CheckImmediate);
  // ...
}

有关于immediate的总共有四句,接下来我们逐个详细介绍一下:

  • uv_check_init(event_loop(), immediate_check_handle()); 这一句主要是初始化uv_check的handle;
  • uv_unref(reinterpret_cast<uv_handle_t*>(immediate_check_handle())); 这一句主要是解除uv_check的handle在event-loop中的引用,因为我们希望在event-loop中没有活跃handle的时候自动退出;
  • uv_idle_init(event_loop(), immediate_idle_handle()); 这里涉及到uv_idle的概念,uv_idle总是在uv_prepare阶段之前运行,在这里是用uv_idle_init方法对uv_idle的handle进行初始化;
  • uv_check_start(immediate_check_handle(), CheckImmediate);在这里,真正的把上文提到的processImmediate通过函数CheckImmediate注册到了uv_check中

uv_idle简介

在这里涉及到了uv_idle系列api,那么就给大家稍作介绍:
相信很多人都看过node官方那张经典的event-loop的图,在idle、prepare阶段的下一阶段是poll。而poll阶段是不断轮训执行callback,所以是会阻塞的。具体的调用代码是uv__io_poll(loop, timeout);,这里的timeout就是超时时间,具体设置timeout的代码可以看这里:

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;
  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;
  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;
  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;
  if (loop->closing_handles)
    return 0;
  return uv__next_timeout(loop);
}

可以看到在一些情况下超时时间可以是0——即可以直接跨过poll阶段到达下一个check阶段,而check阶段就是setImmediate执行的阶段。这些可以跨过poll阶段的情况有:

  • 使用stop_flag直接强制跨过;
  • event-loop中没有活跃的handle且没有活跃的请求时;
  • idle不为空的时候;
  • pending_queue不为空的时候(uv__io_init会初始化pending_queue);
  • 关闭handle的时候;

setImmediate正是利用了idle,实现了对poll阶段的跨越。

setImmediate与uv_idle

在src/env.cc中有一个比较不起眼的api–ToggleImmediateRef:

void Environment::ToggleImmediateRef(bool ref) {
  if (ref) {
    // Idle handle is needed only to stop the event loop from blocking in poll.
    uv_idle_start(immediate_idle_handle(), [](uv_idle_t*){ });
  } else {
    uv_idle_stop(immediate_idle_handle());
  }
}

不知道大家还记得上文提到过的SetupTimers吗,里面有一行代码:

static void SetupTimers(const FunctionCallbackInfo<Value>& args) {
// ...
 auto toggle_ref_cb = [] (const FunctionCallbackInfo<Value>& args) {
      Environment::GetCurrent(args)->ToggleImmediateRef(args[0]->IsTrue());
    };
//...
}

结合这两个函数,很容易得出结论:setImmediate通过函数ToggleImmediateRef对uv_idle进行开关的控制,开的时候可以直接越过poll阶段,关的时候则执行poll阶段

setImmediate的执行

刚才聊到了,通过uv_check_start(immediate_check_handle(), CheckImmediate);setImmediate的上层函数processImmediate通过CheckImmediate注册到了uv_check中。接下来我们看下CheckImmediate

// src/env.cc
void Environment::CheckImmediate(uv_check_t* handle) {
  Environment* env = Environment::from_immediate_check_handle(handle);
  // ...
  do {
    MakeCallback(env->isolate(),
                 env->process_object(),
                 env->immediate_callback_function(),
                 0,
                 nullptr,
                 {0, 0}).ToLocalChecked();
  } while (env->immediate_info()->has_outstanding());

  if (env->immediate_info()->ref_count() == 0)
    env->ToggleImmediateRef(false);
}

相信如果读者从头到尾贯穿下来的话,这里已经很明了了,通过一个do...while...实现了对immediate_callback_function的调用,即调用了js中的processImmediate进而实现了setImmediate的运行。在运行完后,通过env->ToggleImmediateRef(false);实现对uv_idle的停止,进而使得poll能阻塞处理回调。

结语

通过上面的分析,读者基本可以清晰了解到setImmediate的整体注册和触发流程。而真正触发evnet-loop的,则是在src/node.cc中:

// src/node.cc
// ...
inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  // ...
  Environment env(isolate_data, context);
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);// 触发Environment::Start
  // ...
  {
    // ..
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT); // 触发event-loop
    } while (more == true);
  }
  // ...
}

by 小菜
原文地址:https://github.com/xtx1130/blog/issues/19,欢迎star和watch。如果叙述或者逻辑有问题,还请大家斧正。

2 回复

讲的很详细,大佬

@sunstdot 社会,社会。。。

回到顶部