上一篇文章中我献上了一段神奇的代码,并给出了运行结果,也分析了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是高利用率的,作业密集度也是相当高的,一旦被阻塞将会导致性能大幅度下降,这对于服务器端的程序是不愿意看到的。
这个循环将会把所有注册的回调依次执行,这里有两个重点,一个是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 process
void 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
还引用着proxy
的Stub
,而且runtime必须保持一个跟实际发生动作的JS堆栈一样的队列状态信息的副本,毕竟执行与否是在runtime代码进行判断,这个副本作为一个fields数组称为TickInfo类的一个成员。
然后我们在JS层可以轻松完成绑定,
const tickInfo = process._setupNextTick(_tickCallback, _other_tasks);
这下nextTick的proxy Stub
已经绑定到env
里面了,使得一切都变的简单,那么什么时候这个tick_callback_function
被取出来调用呢,
在node中,这个成员被访问以及调用只有在MakeCallback
函数里面,而这个函数有两个版本(其实代码都是类似的), AsyncWrap::MakeCallback
和node::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 callback
CHECK(args[0]->isObject());
CHECK(args[1]->IsFunction());
//ref to this target as persistent
handle.Reset(args[0]);
//turn into weak reference
handle.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
...
//delete the Wrapper C++ object when no other strong references
//to this handle
static 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 optimization
let state = '';
//register a nextTick callback
process.nextTick(() => {
state = 'preemption ! ';
});
//use the binding function implemented in addon
weak(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 joke
template<typename T>
void NeedScope(T&);
//instantiate the template with AsyncCallbackScope by accident
//counter++
NeedScope(env());
还要注意的就是TickInfo这个类,这是一个JS堆栈上异步队列状态的一个镜像副本,控制着nextTick任务的调度,所以它也是不能被拷贝或者移动的,否则多个副本也是会导致混乱,
//another joke
//move assignment
TickInfo ref = std::move(tick_info); //return static_cast<typename std::remove_reference<T>::type&&>(tick_info);
//or in a method without NRV optimization
void 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在setImmediate
的Stub
执行时被添加到了异步队列,所以Stub
执行完在AsyncWrap::MakeCallback
函数里面调用了nextTick注册的额回调,因为是一个循环,所以这个插队行为造成了loop一个微小的阻塞,堵在IO polling
之前,所以得等到nextTick的任务执行完才能看到readFile
的结果,然而问什么同样是nextTick,为什么第一个无论怎么修改延时参数,总是最快执行的呢,原因很简单,它并没经过我们的loop,在脚本执行的阶段就已经先执行了,然后再次才是loop里面的一些异步代码。
仍记得node启动的时候是先将各个模块的代码wrap
成function
,传递module
,exports
,require
三个参数,加载到内存,并进行以文件名为key的键值对缓存,便于运行时加载更加快速,然后再
//call the module function
wrappedScript.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的阶段,我们的模块代码在第一个context
(global
)被执行,所以我们这个时候已经向异步队列里面添加了第一个nextTick回调,然而神奇的是,这个阶段完成即将执行loop里面的回调时,node却自动调用了回调,并且认为这就是loop的第一个phase
,代码都写成这样了,我也没好说什么了,所以我们代码最开始的回调很荣幸被最先执行。
Victory
OK,到这里相信大家对神奇代码的结果也有了比较清楚的理解了,也许也会觉得node引擎离自己的距离更近了,好的,独操引擎的代码之旅到这里就要告一段落啦~~针对V8,还可以看看我另一篇JS对象模型的文章,本系列的文章依据都是调试log实验的结果,当然理解上可能有所疏漏,欢迎各位大神的指正~~独操引擎的男人,永不停步~