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

node源码粗读(7):nextTick和microtasks从bootstrap到event-loop全阶段解读 #16

Open
xtx1130 opened this issue Jan 29, 2018 · 7 comments

Comments

@xtx1130
Copy link
Owner

xtx1130 commented Jan 29, 2018

这篇文章主要介绍nextTick和RunMicrotasks的主要流程和涉及到的相关源码,对于timers相关api在event-loop中的表现不做解读

nextTick实现

目光直接转移到next_tick.js,整体nextTick的代码其实很容易理解:

 const [
    tickInfo,
    runMicrotasks
  ] = process._setupNextTick(_tickCallback);
function nextTick(callback) {
   // ...
   nextTickQueue.push(new TickObject(callback, args, getDefaultTriggerAsyncId()));
  }
function _tickCallback() {
    let tock;
    do {
      while (tock = nextTickQueue.shift()) {
      // ...
      const callback = tock.callback;
        if (tock.args === undefined)
          callback();
      runMicrotasks();
    } while (nextTickQueue.head !== null || emitPromiseRejectionWarnings());
    tickInfo[kHasPromiseRejections] = 0;
  }

通过这两个函数,就能看出来整个nextTick是如何工作的。

  • nextTickQueue为记录nextTick的数组,有新的nextTick注册进来就会被推入数组
  • _tickCallback则会不断的推出数组中的元素然后运行

大家注意一下process._setupNextTick(_tickCallback),最终这个_tickCallback并没有在js中执行,而是传递给了c++:

// node.cc
void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args[0]->IsFunction());
  env->set_tick_callback_function(args[0].As<Function>());
  // ...

在这里可以看出来,最终_tickCallback丢给了tick_callback_function,然后在LoadEnvironment中通过_setupNextTick触发运行(LoadEnvironment之前详细介绍过,在这里不做过多介绍),在这里简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了tick_callback_function

process.nextTick(()=>console.log(2))

issue16-1

tips: 蓝色底色代码为断点所在位置,下方为此时刻的内存地址,上面这张图可以看出来在没有跑LoadEnvironment的时候,tick_callback_function为NULL

issue16-2
如果对LoadEnvironment比较了解的读者,应该是明白其中的原理的,如果不明白原理可以简单看一下tick_callback_function这里的内存变化。这里我们假设读者了解node启动的所有机制,那么就会发现一件事情:process.nextTick运行的时候,uv_run尚未启动
那么,我们可以根据这个显现得出一个比较浅显的结论:process.nextTick会阻塞libuv的事件循环。(这是在node初始化bootatrap阶段的情况。即使在evnt_loop中,表现也是一样的。为何用这个阶段来叙述,是因为这个阶段最容易追踪和解读)

process.nextTick和RunMicrotasks

通过前一章节的叙述和上一篇文章对setTimeout流程的分析,我们可以发现:process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?
接下来就要从nextTick的源码聊起了:

function _tickCallback() {
    let tock;
    do {
      while (tock = nextTickQueue.shift()) {
      // ...
      const callback = tock.callback;
        if (tock.args === undefined)
          callback();
     // ...
     }
      runMicrotasks();
    } 
  // ...
  }

在执行完nextTick之后(callback())还继续执行了runMicrotasks,我相信如果了解过Microtasks的读者肯定知道这到底是做什么的,接下来我们深扒一下这个runMicrotasks

// src/node.cc
v8::Local<v8::Function> run_microtasks_fn =
      env->NewFunctionTemplate(RunMicrotasks)->GetFunction(env->context())
          .ToLocalChecked();//v8 吐出来的方法 RunMicrotasks
run_microtasks_fn->SetName(
      FIXED_ONE_BYTE_STRING(env->isolate(), "runMicrotasks"));

// deps/v8/src/isolate.cc
void Isolate::RunMicrotasks() {// v8中RunMicrotasks实现
  // Increase call depth to prevent recursive callbacks.
  v8::Isolate::SuppressMicrotaskExecutionScope suppress(
      reinterpret_cast<v8::Isolate*>(this));
  is_running_microtasks_ = true;
  RunMicrotasksInternal();
  is_running_microtasks_ = false;
  FireMicrotasksCompletedCallback();
}
void Isolate::RunMicrotasksInternal() {
  if (!pending_microtask_count()) return;
  TRACE_EVENT0("v8.execute", "RunMicrotasks");
  TRACE_EVENT_CALL_STATS_SCOPED(this, "v8", "V8.RunMicrotasks");
  while (pending_microtask_count() > 0) {
    HandleScope scope(this);
    int num_tasks = pending_microtask_count();
    Handle<FixedArray> queue(heap()->microtask_queue(), this);
    DCHECK(num_tasks <= queue->length());
    set_pending_microtask_count(0);
    heap()->set_microtask_queue(heap()->empty_fixed_array());
  // ...

通过上面的代码,可以比较清晰地看到整个RunMicrotasks的全过程,主要就是通过microtask_queue来实现的Microtask。
了解了整个流程,可以很容易得出一个结论:nextTick会在v8执行Microtasks之前对在js中注册的nextTickQueue逐个执行,即阻塞了Microtasks执行。

bootstrap阶段和event-loop时候的异同

通过上面的分析,下面这段代码在bootstrap阶段,应该很容易理解:

setTimeout(()=>console.log('timers API'),0)//uv_run开始运行后才执行timers相关api,最后执行
console.log('bootstrap')//在node LoadEnvironment(bootstrap)阶段执行,最先执行
new Promise((resolve,reject)=> resolve('microtask run')).then(arg => console.log(arg))//注册到microtask_queue中
process.nextTick(()=>console.log('run next tick'))// 会在microtask之前运行

结果如图:
issue16-3
相关解释已经写到了上面的注释中。 (当然这里用console来作为同步代码不是很严谨,不过比较直观)

那么在event-loop中是如何表现的呢?在上文中也提到过一句:

这是在node初始化,即bootstrap的情况下,即使在evnt_loop中,表现也是一样的

event-loop中的区别是:本应该在node LoadEnvironment(bootstrap)阶段执行的代码的运行转移到了InternalMakeCallback
下面是InternalMakeCallback的代码:

// ./src/node.cc
MaybeLocal<Value> InternalMakeCallback(Environment* env,
                                       Local<Object> recv,
                                       const Local<Function> callback,
                                       int argc,
                                       Local<Value> argv[],
                                       async_context asyncContext) {
  CHECK(!recv.IsEmpty());
  InternalCallbackScope scope(env, recv, asyncContext);
  if (scope.Failed()) {
    return Undefined(env->isolate());
  }

  MaybeLocal<Value> ret;

  {
    ret = callback->Call(env->context(), recv, argc, argv);
    // ...
  }
 // ...
  return ret;
}

通过ret = callback->Call(env->context(), recv, argc, argv);实现了event-loop中主体代码的运行,之后在InternalMakeCallback结束之后,实现对nextTick和microtask的调用,代码如下:

// ./src/node.cc
void InternalCallbackScope::Close() {
  // ...
  Environment::TickInfo* tick_info = env_->tick_info();

  if (!tick_info->has_scheduled()) {
    env_->isolate()->RunMicrotasks();
  }
  // ...
  if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
    return;
  }
  // ...
  Local<Object> process = env_->process_object();

  if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
    failed_ = true;
  }
}

其中,有两个需要注意的地方,一个是:

 if (!tick_info->has_scheduled()) {
    env_->isolate()->RunMicrotasks();
  }
 // ...
  if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
    return;
  }

这两处代码专门针对无process.nextTick行为的event-loop进行了处理,直接从node中调用v8的RunMicrotasks,加快整体处理速度。
另外一个地方是:

if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
    failed_ = true;
  }

通过对tick_callback_function的调用,实现触发之前讲过的_tickCallback,不知道大家还记得这句话么:

在这里简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了tick_callback_function

这样,整体形成了一个闭环,无论是bootstrap阶段还是在event-loop阶段,总是能保证两点:

  • nextTick永远在主函数(包括同步代码和console)运行完之后运行
  • nextTick永远优先于microtask运行

by 小菜

@youth7
Copy link

youth7 commented Feb 22, 2018

有个地方想请教一下,node 的官方文档写明event loop分为timer、io callbacks 、poll几个phrase,在code.c的uv_run函数中,确实是依次执行了这几个phrase。文档中还指出process.nextTick不属于上面任何一个phrase,而是在每个phrase的执行之后都会执行,这相当于uv_run每执行完一个phrase就被阻塞了,感觉精确阻塞一个函数好像是不可能的,不知道这在源码层面是如何实现,请指教

@xtx1130
Copy link
Owner Author

xtx1130 commented Feb 22, 2018

@youth7 这篇文章提过一句:

通过ret = callback->Call(env->context(), recv, argc, argv);实现了event-loop中主体代码的运行,之后在InternalMakeCallback结束之后,实现对nextTick和microtask的调用

通过这篇文章和上一篇文章能关联出来:上篇文章中说过OnTimeout函数,这个函数最终调用了MakeCallback ,而MakeCallback 最终调用了InternalMakeCallback ,InternalMakeCallback 之后会调用InternalCallbackScope::Close()

@youth7
Copy link

youth7 commented Feb 23, 2018

@xtx1130
没错,关键之处是InternalMakeCallback会调用InternalCallbackScope::Close()InternalCallbackScope::Close()里面的env_->tick_callback_function()最终实现了nextTick的callback的运行。不过我还有的疑问是,如果上面说的是正确的,那么InternalMakeCallback每次都会调用Close,这意味着每个phrase的每个callback运行之后都会触发nextTick的callback,这貌似跟

每个phrase的callback执行完毕之后才触发nextTick的callback

有点不符合,不知道我是错过了什么地方

@xtx1130
Copy link
Owner Author

xtx1130 commented Feb 23, 2018

@youth7

每个phrase的每个callback运行之后都会触发nextTick的callback

(下面的表述是基于setTimeout或者setInterval中)
一个phrase是通过InternalMakeCallback 运行,运行完之后调用InternalCallbackScope::Close()。我不太明白你这句话要表达什么意思。InternalMakeCallback运行的就是他们注册的函数,通过上一篇文章你也能看出来,最终timers api的执行逻辑都是在js中,而node是作为v8和libuv的桥梁的存在,而一个phrase中运行的是此时间点注册的所有函数,在运行完这个时间点所有注册的函数之后,会执行nextTick中注册的函数。你想表达的意思是运行之后执行完毕之后这个措辞的问题吗?

@tsy77
Copy link

tsy77 commented Aug 28, 2018

你好,麻烦问下

setTimeout(()=>console.log('timers API'),0)//uv_run开始运行后才执行timers相关api,最后执行
console.log('bootstrap')//在node LoadEnvironment(bootstrap)阶段执行,最先执行
new Promise((resolve,reject)=> resolve('microtask run')).then(arg => console.log(arg))//注册到microtask_queue中
process.nextTick(()=>console.log('run next tick'))// 会在microtask之前运行

这段逻辑为什么run next tick和microtask run会在uv_run之前执行呢?Node官网也有说明,在event loop前会清空next_tick队列,但是在源码上没有找到

@xtx1130
Copy link
Owner Author

xtx1130 commented Aug 29, 2018

@tsy77 在bootstrap阶段触发就是通过process._tickCallback();触发的,你可以查看一下./lib/internal/bootstrap/node.js的代码。只有在event loop中才会在c++中通过InternalCallbackScope::Close 触发

@tsy77
Copy link

tsy77 commented Sep 5, 2018

@xtx1130 了解了 感谢!

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

3 participants