神之门V8-----Event loop的舞池盛宴(3)

来源:互联网 发布:.liu域名 编辑:程序博客网 时间:2024/04/29 10:49

上一篇文章中我献上了一段神奇的代码,并给出了运行结果,也分析了setTimeout这个函数,在本文,我还会带领大家继续分析setImmediate函数以及nextTick原理以及MakeCallback函数跟contextify的问题。

OK,跟着独操引擎的男人继续我们的代码之旅吧~~

这里写图片描述

setImmediate

setImmediate函数的callback production机制跟setTimeout类似,只是consumer触发的机制有点新意,跟前面的思路一样,这个触发肯定是从loop的某个phase发出的,不过它是一个跟process对象相关的结构,也没有再专门实现一个表示Immediate的类继承自HandleWrap,所以它不能再像以前那样通过JS对象跟一个C++ addon 对象进行binding,没有binding就意味着没有handle来绑定JS堆栈的Stub,另外,它也需要一个途径将C++的回调注册到loop里面,并且这个操作必然需要实现在C++ binding callback上,这个不是很理解的可以先学习一下node c++ addon,所以我们很显然就会考虑到process这个对象,就它活跃着,不利用它有点对不起node的设计,我们可以这样完成JS function 的注册,

  var immediate = new Immediate();  immediate._callback = callback;  immediate._argv = args;  immediate._onImmediate = callback;  if (!process._needImmediateCallback) {    process._needImmediateCallback = true;    process._immediateCallback = invoke_immediate_callback_finally;  }

能成为binding callback的主要由两类,一个是JS的binding函数,它的调用其实在C++ runtime的代码里,以一个FunctionCallbackInfo<T> 类封装这个JS function的信息,一个是property accessor 在赋予binding object 属性时触发,以一个PropertyCallbackInfo<T> 类封装参数,这里我们选择process对象作为一个proxy,所以上面的代码可以看到我们把最终的proxy callback直接赋值给process_immediateCallback 属性上就可以触发相应的runtime setter代码,其中属性的名字是个约定好的名字,不能随便在C++层更改,当process对象在node启动被构造出来它的getter和setter也会被挂载,

  //as process constructed from a v8::FunctionTemplate<T>  //let the compiler infer the real type of the returned value  auto process_template = FunctionTemplate::New(isolate());  process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));  auto process_object = process_template->GetFunction()->NewInstance(context()).ToLocalChecked();  set_process_object(process_object);  //setup setter and getter for process  auto maybe = process->SetAccessor(env->context(),                               //return _needImmediateCallback                               env->need_imm_cb_string(),                               NeedImmediateCallbackGetter,                               NeedImmediateCallbackSetter,                               env->as_external());

所以我们需要将在setter里面启动loop的这一个phase,回想上一片libuv的loop过程,setImmediate是被注册在最后一帧的check event上面,注意,上面查找env字符串哪个宏返回的是_needImmediateCallback ,所以我们在代码中只要先设定了此属性(设为true)就可以触发setter,至于设定第二个属性是为了标志回调,

这里写图片描述

  uv_check_t* immediate_check_handle = env->immediate_check_handle();  //use static_cast<> can be safe  bool active = uv_is_active(      reinterpret_cast<const uv_handle_t*>(immediate_check_handle));  if (active == value->BooleanValue())    return; /*for env has unreferenced all idle prepare,check handles when                 initialized,so that these handles are not active, if active, we will stop these handle avoiding loop's polling */  uv_idle_t* immediate_idle_handle = env->immediate_idle_handle();  if (active) {    uv_check_stop(immediate_check_handle);    uv_idle_stop(immediate_idle_handle);  } else {    //start to handle the check events    uv_check_start(immediate_check_handle, CheckImmediate);    // Idle handle is needed only to stop the event loop from blocking in poll.    uv_idle_start(immediate_idle_handle, IdleImmediateDummy);  }

看注释大家应该可以理解这个Immediate事件是怎么被注册到loop的最后一帧,然后相同的套路,我们还需要一个真正执行JS proxy的C++ 回调,就是那个CheckImmediate 函数,还是调用那个node::MakeCallback 函数,里面把process当作recv参数,即从这个对象的JS Object Map 上读取一个属性作为回调函数的proxy Stub ,负责把JS堆栈上注册的回调逐个执行,这次env的这个宏返回的就是_immediateCallback 了,也就是调用我们JS代码里面设定的那个proxy 函数,OK,setImmediate函数就这样完成自己的回调执行,很明显,这样的异步做法同样会造成调用堆栈的缺失。

node::MakeCallback(env, env->process_object(), env->immediate_callback_string());

回想那段神奇的代码,setImmediate在JS脚本执行的时候(其实脚本的启动是在模块readFileSync到内存进行缓存构建时调用了vm.RunInThisContext())在vm中(仍然在JS堆栈上)执行了我们的业务代码,所以这时候loop刚启动一会,注册的操作就随着process的构建进入了loop里面,由于是在loop的最后一帧(poll event之后),所以在loop刚到check阶段时,这个事件已经就绪,所以它会是在loop里面触发的最早执行回调的一个,然而里面的nextTick操作看起来挡在poll event之前,我们再来看看插队的nextTick。

插队的nextTick

process.nextTick的注册非常简单,直接放到一个nextTickQueue就完事了,重点是看看这个队列是怎么被consume的,我们先来讨论在C++层发生的事情,即找到consume的地方,为了在JS堆栈执行回调,先实现一个JS function,

function _tickCallback() {    var callback, args, tock;    do {      /*tickinfo is a C++ binding object from which we read the status of nextTickQueue,it has been initialized as a fixedArray full of 0 , and it was set at JS stack instead the runtime */      //loop util no callback in queue      while (tickInfo[kIndex] < tickInfo[kLength]) {        tock = nextTickQueue[tickInfo[kIndex]++];        callback = tock.callback;        args = tock.args;        // Using separate callback execution functions allows direct        // callback invocation with small numbers of arguments to avoid the        // performance hit associated with using `fn.apply()`        /*for read the property in object map with searching at prototype chain need to be optimized by IC,so if compiler know the exact number of arguments and calling directly as a normal invocation will be faster than calling as 'fn.apply()' */        _combinedTickCallback(args, callback);        //too many nextTick to handle will block our loop so the polling events can not be resolved        if (1e4 < tickInfo[kIndex])          tickDone();      }      //finish all callback tasks      tickDone();      //stuffs need to be handled sometimes      emitPendingUnhandledRejections();    } while (tickInfo[kLength] !== 0);  }

根据我的注释,在这里优化了一下代码,即为少数参数的情况下直接调用函数,这样编译器会为最常用的情况生成快速执行(避开的原型链的查找)的Stub,当然那几种也确实是编译器一个辅助线程抽样检测栈帧得到的最常用的调用情况,我们可以直接在JS代码里面这么写以便编译器的缓存优化效果更好(即时没有进入优化模式,也是可以保证快速的运行),至于最后,我们需要顺便处理一下一些没有处理而且reject状态的promise,它们会emit一个事件来通知我们,当然我们可以不监听它,它们不会长期存在,也不会在没有处理的情况下继续苟延残喘,它们作为key对象存储在WeakMap中,好处就是key都是弱引用,最后还是会被GC清理。为了不阻塞IO polling,这里也为nextTick设定了一个上限,不然插队的nextTick的一发不可收拾(一旦递归起来),loop就会被阻塞,单线程的loop对于CPU是高利用率的,作业密集度也是相当高的,一旦被阻塞将会导致性能大幅度下降,这对于服务器端的程序是不愿意看到的。

horrible cost of IO operation

这个循环将会把所有注册的回调依次执行,这里有两个重点,一个是TickInfo这个类(是Enviroment的内部类),一个是这个consume函数是怎么传递到runtime的,这个问题很简单,本来可以故技重施利用process作为proxy进行属性绑定,然而考虑到我们要在AsyncWrap::MakeCallback 调用nextTick的Stub 并不是那么方便引用process,但有一个家伙出场率特别高,就是Enviroment对象env ,很好,我们用一个Persistent<T>引用这个JS function proxy ,并作为env的一个成员,然后还需要一个binding函数,接受一个JS function作为参数,顺便初始化一个记录队列状态的TickInfo结构,(本质是一个数组类型,第一个元素存放当前JS callback在队列中的索引,第二个元素存放队列的长度,我们从第一段代码可以看到这个tickinfo),OK,perfect,

//binding this to function to processvoid SetupNextTick(const FunctionCallbackInfo<Value>& args) {  Environment* env = Environment::GetCurrent(args);  CHECK(args[0]->IsFunction());  //binding  env->set_tick_callback_function(args[0].As<Function>());  env->process_object()->Delete(      env->context(),      FIXED_ONE_BYTE_STRING(args.GetIsolate(), "_setupNextTick")).FromJust();  // Values use to cross communicate with processNextTick.  uint32_t* const fields = env->tick_info()->fields();  uint32_t const fields_count = env->tick_info()->fields_count();  //treat fields as the status of queue,the Handle constructed with the real pointer to this object will keep communication with status hold at runtime code at JS stack  Local<ArrayBuffer> array_buffer =      ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count);  args.GetReturnValue().Set(Uint32Array::New(array_buffer, 0, fields_count));}

runtime其实后面会继续引用这个返回对象,并不是一次 temporary copy ,对象不会像一般的return执行完后那样被销毁,args还引用着proxyStub ,而且runtime必须保持一个跟实际发生动作的JS堆栈一样的队列状态信息的副本,毕竟执行与否是在runtime代码进行判断,这个副本作为一个fields数组称为TickInfo类的一个成员。

然后我们在JS层可以轻松完成绑定,

const tickInfo = process._setupNextTick(_tickCallback, _other_tasks);

这下nextTick的proxy Stub已经绑定到env里面了,使得一切都变的简单,那么什么时候这个tick_callback_function 被取出来调用呢,

在node中,这个成员被访问以及调用只有在MakeCallback函数里面,而这个函数有两个版本(其实代码都是类似的), AsyncWrap::MakeCallbacknode::MakeCallback ,好吧,貌似见过她好几次了,是不是觉得前面每一个注册到loop的回调都会通过这个函数调用Stub,没错,这个nextTick的调用其实就安插在每一个phase的最后,换句话说,就是插队。。。

X::MakeCallback(x, 0, nullptr);

这里写图片描述

所以看起来,它会阻塞loop,一般会卡在IO polling之前,所以我们每一phase的结束都会伴随着回调的执行。

其实第一个版本是最常发生的,我们手动设定的nextTick基本都是发生在那里(几乎所有的异步事件的回调),那第二个版本就有点尴尬了,它一般发生在node进程即将exit的时候,会苟延残喘再次触发nextTick执行剩余没有来得及处理的回调(在node::EmitBeforeExit 函数和node::EmitExit函数),还有就是我们看不到的GC为弱引用调用的weakCallback, 通知程序这个对象没有其它引用了,(这个时候我们可以选择 delete ptr 或者 handle.ClearWeak(), 继续当一个Persistent引用 )我们来看看MakeCallback函数,最后还有我们刚刚讨论的setImmediate函数啦~

Local<Value> MakeCallback(Environment* env,                          Local<Value> recv,                          const Local<Function> callback,                          int argc,                          Local<Value> argv[]) {    //do something to support hooks and domain    //a 'lock' like mechanism to avoid some method out of control call this method result in preemption    Environment::AsyncCallbackScope callback_scope(env);    if (recv->IsObject()) {        object = recv.As<Object>();    }    //invoke the callback we pass in first    //all the proxy Stub invoked here    Local<Value> ret = callback->Call(recv, argc, argv);    if (ret.IsEmpty()) {    // NOTE: For backwards compatibility with public API we return Undefined()    // if the top level call threw.        return callback_scope.in_makecallback() ?            ret : Undefined(env->isolate()).As<Value>();    }    //if the internel counter > 1    //plus one at the previous stack phase,which can not call nextTick    if (callback_scope.in_makecallback()) {        return ret;    }    //get status of the queue    Environment::TickInfo* tick_info = env->tick_info();    if (tick_info->length() == 0) {        //run microtasks    }    //we still need 'process' as the context of 'process.nextTick()'    Local<Object> process = env->process_object();    //empty queue, reset    if (tick_info->length() == 0) {        tick_info->set_index(0);    }    //invoke the proxy Stub so all the callback in queue get called    //set context to process, or it will loss its context,destroy the contextify    if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {        return Undefined(env->isolate());    }    return ret;}

Preemption ?

这里有个异步回调计数的问题,为什么要搞这个呢,这有点像线程的自旋锁,避免Threads Race ,但这个单线程模型的代码是无锁的,它只是避免回调抢占(preemption),举个简单的例子,比如上面说的弱引用,GC是很难告诉我们在什么时候回收资源的,当没有其它非弱引用引用这个资源,它的WeakCallback就会被调用,

这里写图片描述

当然这必须要针对JS 堆栈的Stub,runtime设定的弱引用回调肯定不会这么愚蠢地调用node::makeCallback ,这个函数都是提供给我们在runtime上异步调用JS回调,如果我们没有加上异步回调计数,可以写一个addon完成弱引用的转化,

//write a wrapper class named Y//Y::New//this handle can be the context of VM for 'contextify'Persistent<Object> handle;//accept a target and a Stub for weak callbackCHECK(args[0]->isObject());CHECK(args[1]->IsFunction());//ref to this target as persistenthandle.Reset(args[0]);//turn into weak referencehandle.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);...//delete the Wrapper C++ object when no other strong references//to this handlestatic void WeakCallback(const WeakCallbackInfo<Y>& data) {    Y* wrapper = static_cast<Y*>(data.GetParameter());    delete wrapper; } 

然后在JS堆栈上建立一个弱引用,再手动调用GC(stop the world,像上图红色部分),然后就可以触发我们的node::makeCallback,引发一次nextTick的动作,我们会出现意想不到的结果,

//fixed type for optimizationlet state = '';//register a nextTick callbackprocess.nextTick(() => {    state = 'preemption ! ';});//use the binding function implemented in addonweak(obj, () => {    //this is a Stub passed to 'args[1]'    //release some refs on stack});state = 'original value';//for we need to call garbage collector manually, we run adding //adding a --expose-gc option global.gc();//the state = 'preemption' here//but what we expect is that after this phase of loop while //running scripts not now//it failed!!assert(state === 'original value');

所以为了避免这种bug,需要判断一下计数,不要随便让nextTick插队,这个scope是一个分配在栈上的对象,在离开函数作用域就会被析构掉,在它的析构函数中又回把增加的计数减回去,所以如果没有以前增加计数的话,每一次都会发生插队,不过这个计数机制让我们有办法避免这种事情,我们只需要在调用MakeCallback的上一个栈帧构造一个scope,让计数器先增加一就可以,比如在所有弱引用回调调用的入口,

//entry of all weak callback in GC//create a new scope{    Enviroment::AsyncCallbackScope _callback_scope_(isolate->env());    //in the constructor    //env()->_makecallback_cntr++;    //weak callback notification}

No Copy

这样我们就可以避免preemption的问题,不过要注意这个类的构造函数要声明为explicit,因为是个单参数的构造函数,需要防止隐式的构造,造成莫名其妙的计数错误,导致nextTick没发正常执行,

//just a joketemplate<typename T>void NeedScope(T&);//instantiate the template with AsyncCallbackScope by accident//counter++NeedScope(env());

还要注意的就是TickInfo这个类,这是一个JS堆栈上异步队列状态的一个镜像副本,控制着nextTick任务的调度,所以它也是不能被拷贝或者移动的,否则多个副本也是会导致混乱,

//another joke//move assignmentTickInfo ref = std::move(tick_info); //return static_cast<typename std::remove_reference<T>::type&&>(tick_info);//or in a method without NRV optimizationvoid no_nrv_method() {    //copy assignment    TickInfo temporaty_info = env()->tick_info();    ...    //maybe move constructor    //temporary_info.TickInfo::~TickInfo();    //copy or move constructor called     return temporary_info; }

像上面这些比较荒唐的情况就会产生一些附加的运行时对象,一时间可能导致多出来好几个多余的副本,情况比较危险,虽然讲道理我们没有自己定义析构函数而且它的成员也没有显式定义移动拷贝等构造函数,编译器不会给我们自己合成,但是安全的做法就是斩草除根,定义一个宏将构造移动拷贝函数以及运算符定义为删除函数,这样如果有哪个无知的家伙在代码中定义了它们在编译阶段就会当做不合法,

//add this macro to all class which need to delete method#define DISALLOW_COPY_AND_ASSIGN(TypeName)      \                              void operator=(const TypeName&) = delete;     \                               void operator=(TypeName&&) = delete;          \                               TypeName(const TypeName&) = delete;           \  TypeName(TypeName&&) = delete;                             

Entry NextTick

现在我们应该基本清楚神奇代码的结果了,nextTick在setImmediateStub执行时被添加到了异步队列,所以Stub执行完在AsyncWrap::MakeCallback函数里面调用了nextTick注册的额回调,因为是一个循环,所以这个插队行为造成了loop一个微小的阻塞,堵在IO polling之前,所以得等到nextTick的任务执行完才能看到readFile的结果,然而问什么同样是nextTick,为什么第一个无论怎么修改延时参数,总是最快执行的呢,原因很简单,它并没经过我们的loop,在脚本执行的阶段就已经先执行了,然后再次才是loop里面的一些异步代码。

仍记得node启动的时候是先将各个模块的代码wrapfunction,传递moduleexportsrequire三个参数,加载到内存,并进行以文件名为key的键值对缓存,便于运行时加载更加快速,然后再

//call the module functionwrappedScript.RunInThisContext()

这个过程再模块加载器的Module.runmain函数里面,

  // bootstrap main module.  Module.runMain = function() {  // Load the main module--the command line argument.  Module._load(process.argv[1], null, true);  // Handle any nextTicks added in the first tick of the program  process._tickCallback();

看到最后一行大家估计就明白一切是怎么回事了,上述的阶段基本发生在load的阶段,我们的模块代码在第一个contextglobal)被执行,所以我们这个时候已经向异步队列里面添加了第一个nextTick回调,然而神奇的是,这个阶段完成即将执行loop里面的回调时,node却自动调用了回调,并且认为这就是loop的第一个phase,代码都写成这样了,我也没好说什么了,所以我们代码最开始的回调很荣幸被最先执行。

Victory

OK,到这里相信大家对神奇代码的结果也有了比较清楚的理解了,也许也会觉得node引擎离自己的距离更近了,好的,独操引擎的代码之旅到这里就要告一段落啦~~针对V8,还可以看看我另一篇JS对象模型的文章,本系列的文章依据都是调试log实验的结果,当然理解上可能有所疏漏,欢迎各位大神的指正~~独操引擎的男人,永不停步~

1 0
原创粉丝点击