结合源码分析 setTimeout / setInterval / setImmediate / process.nextTick 执行时机
发布于 5 个月前 作者 hunjixin 1260 次浏览 来自 分享

环境准备

工具: git/cmake/vscode(安装js和C++插件)/python

vscode :https://code.visualstudio.com/Download

编译参考https://github.com/nodejs/node/blob/master/BUILDING.md

需要注意的是为了调试方便,需要在make命令中开启debug,如果觉得编译过程慢也可以适当调大并发数。

  make -d j4

命令行:在根目录输入:

        gdb node    // 调试目标文件
        break main  // 在main里加断点
        run test.js // 开始执行调试

vscode:编译完成后用vscode打开,配置编译项目,这里给出我的launch文件做参考

    {
        "version": "0.2.0",
        "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/test.js"
        },
        {
            "name": "(gdb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceRoot}/out/Debug/node",
            "args": [
                "test.js"
        ],
        "stopAtEntry": true,
        "cwd": "${workspaceRoot}",
        "environment": [

        ],
        "externalConsole": true,
        "MIMode": "gdb",
        "setupCommands": [
            {
               "description": "Enable pretty-printing for gdb",
                   "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ]
        }
        ]
    }

代码分析

setTimeout/setInterval

源码位置:node-master/lib/timers.js

  • setTimeout调用过程 : setTimeout -> createSingleTimeout -> new Timeout() -> active

  • setInterval调用过程 : setInterval -> createRepeatTimeout -> new Timeout -> active

两者最终都是使用的TimeOut对象,其区别在于repeat参数,这个参数控制当前timer是否重用。

	 repeat *= 1; // coalesce to number or NaN
	  if (!(repeat >= 1 && repeat <= TIMEOUT_MAX))
		repeat = 1; // schedule on next tick, follows browser behavior
	
	  var timer = new Timeout(repeat, callback, args);
	  timer._repeat = repeat;
	  if (process.domain)
		timer.domain = process.domain;
	
	  active(timer);
	
	  return timer;

active操作会将当前的timer合并到原来的lists对象上面,这个对象按照timeout的时间分组,key为过期时间,value为一个list,存放计时器对象。

	function insert(item, unrefed) {
	
	   //..................
	
	  // Use an existing list if there is one, otherwise we need to make a new one.
	  var list = lists[msecs];
	  if (!list) {
		debug('no %d list was found in insert, creating a new one', msecs);
		lists[msecs] = list = createTimersList(msecs, unrefed);
	  }
	
	   //..................
	
	  L.append(list, item);
	  assert(!L.isEmpty(list)); // list is not empty
	}

计数器触发时会遍历对应list,逐个执行。调用过程 : listOnTimeout -> tryOnTimeout -> ontimeout

	function listOnTimeout() {
	  var list = this._list;
	  var msecs = list.msecs;
	
	  //...........................
	
	  while (timer = L.peek(list)) {
	   
	  //...........................
	
		tryOnTimeout(timer, list);
	
	  }
	
	  // If `L.peek(list)` returned nothing, the list was either empty or we have
	  // called all of the timer timeouts.
	  // As such, we can remove the list and clean up the TimerWrap C++ handle.
	  debug('%d list empty', msecs);
	  assert(L.isEmpty(list));
	
	  // Either refedLists[msecs] or unrefedLists[msecs] may have been removed and
	  // recreated since the reference to `list` was created. Make sure they're
	  // the same instance of the list before destroying.
	  if (list._unrefed === true && list === unrefedLists[msecs]) {
		delete unrefedLists[msecs];
	  } else if (list === refedLists[msecs]) {
		delete refedLists[msecs];
	  }
	
	  // Do not close the underlying handle if its ownership has changed
	  // (e.g it was unrefed in its callback).
	  if (this.owner)
		return;
	
	  this.close();
	}

setImmediate

setImmediate则是个简单的链表

    function Immediate() {
        // assigning the callback here can cause optimize/deoptimize thrashing
        // so have caller annotate the object (node v6.0.0, v8 5.0.71.35)
        this._idleNext = null;
        this._idlePrev = null;
        this._callback = null;
        this._argv = null;
        this._onImmediate = null;
        this._destroyed = false;
        this.domain = process.domain;
        this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr];
        this[trigger_id_symbol] = initTriggerId();
        if (async_hook_fields[kInit] > 0)
             emitInit(this[async_id_symbol], 'Immediate', this[trigger_id_symbol], this);
    }

setImmediate函数的调用过程: setImmediate -> createImmediate -> immediateQueue.append(immediate);

触发时执行过程: processImmediate -> tryOnImmediate -> runCallback

    function processImmediate() {
        var immediate = immediateQueue.head;
        var tail = immediateQueue.tail;
        var domain;

        // Clear the linked list early in case new `setImmediate()` calls occur while
        // immediate callbacks are executed
        immediateQueue.head = immediateQueue.tail = null;

        while (immediate) {

        //..........................

        immediate._callback = immediate._onImmediate;

        // Save next in case `clearImmediate(immediate)` is called from callback
        var next = immediate._idleNext;

        tryOnImmediate(immediate, tail);

        //..........................

        if (immediate._idleNext)
            immediate = immediate._idleNext;
        else
            immediate = next;
        }
    }

process.NextTick

源码位置:node-master/lib/internal/process/next_tick.js process.NextTick实现为一个队列,nextTick函数做的工作就是把回调函数转换成TickObject,然后进到nextTickQueue队列里面。

    function nextTick(callback) {
		if (typeof callback !== 'function')
		  throw new errors.TypeError('ERR_INVALID_CALLBACK');

		if (process._exiting)
		  return;

		var args;
		switch (arguments.length) {
		  case 1: break;
		  case 2: args = [arguments[1]]; break;
		  case 3: args = [arguments[1], arguments[2]]; break;
		  case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
		  default:
			args = new Array(arguments.length - 1);
			for (var i = 1; i < arguments.length; i++)
			  args[i - 1] = arguments[i];
		}

		const asyncId = ++async_uid_fields[kAsyncUidCntr];
		const triggerAsyncId = initTriggerId();
		const obj = new TickObject(callback, args, asyncId, triggerAsyncId);
		nextTickQueue.push(obj);
		++tickInfo[kLength];
		if (async_hook_fields[kInit] > 0)
		  emitInit(asyncId, 'TickObject', triggerAsyncId, obj);
	  }

nextTick触发时的调用过程 :

_tickCallback(_tickDomainCallback) -> _combinedTickCallback

其中TickInfo类型用于控制每次执行callbck(可以想象成数组上有个指向两端的指针0指向头,1指向尾)

执行nextTick回调函数

	  function _tickDomainCallback() {
		do {
		  while (tickInfo[kIndex] < tickInfo[kLength]) {
			++tickInfo[kIndex];
			const tock = nextTickQueue.shift();
			const callback = tock.callback;
			const domain = tock.domain;
			const args = tock.args;
			if (domain)
			  domain.enter();

			// CHECK(Number.isSafeInteger(tock[async_id_symbol]))
			// CHECK(tock[async_id_symbol] > 0)
			// CHECK(Number.isSafeInteger(tock[trigger_id_symbol]))
			// CHECK(tock[trigger_id_symbol] > 0)

			emitBefore(tock[async_id_symbol], tock[trigger_id_symbol]);
			// TODO(trevnorris): See comment in _tickCallback() as to why this
			// isn't a good solution.
			if (async_hook_fields[kDestroy] > 0)
			  emitDestroy(tock[async_id_symbol]);

			// Using separate callback execution functions allows direct
			// callback invocation with small numbers of arguments to avoid the
			// performance hit associated with using `fn.apply()`
			_combinedTickCallback(args, callback);  //执行tick回调

			emitAfter(tock[async_id_symbol]);

			if (kMaxCallbacksPerLoop < tickInfo[kIndex])
			  tickDone();
			if (domain)
			  domain.exit();
		  }
		  tickDone();
		  _runMicrotasks();
		  emitPendingUnhandledRejections();
		} while (tickInfo[kLength] !== 0);
	  }

EventLoop

Event Loop 简单说就是在主程序里面轮循多个队列,取出其中的回调函数并且执行,再这个过程中实现异步,同时也通过精巧的设计平衡不同类型的任务。使得程序能够尽量无阻塞的运行下去。 代码如下:

    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);

      while (r != 0 && loop->stop_flag == 0) {
        uv__update_time(loop);
        uv__run_timers(loop);
        ran_pending = uv__run_pending(loop);
        uv__run_idle(loop);
        uv__run_prepare(loop);

        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
          timeout = uv_backend_timeout(loop);

        uv__io_poll(loop, timeout);//process.nextTick 和 microtask Promise
        uv__run_check(loop);
        uv__run_closing_handles(loop);

        if (mode == UV_RUN_ONCE) {
          /* 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)
          break;
      }

这里只分析几个相关的过程。 uv__run_timers/uv__run_check

timer对象关联

uv__run_timers -> OnTimeout -> AsyncWrap::MakeCallback

JS 在索引kOnTimeout位置挂上脚本函数listOnTimeout

    function createTimersList(msecs, unrefed) {
      // Make a new linked list of timers, and create a TimerWrap to schedule
      // processing for the list.
      const list = new TimersList(msecs, unrefed);
      L.init(list);
      list._timer._list = list;
        
      if (unrefed === true) list._timer.unref();
      list._timer.start(msecs);
        
      list._timer[kOnTimeout] = listOnTimeout;
        
      return list;
    }

AsyncWrap::MakeCallback中object()->Get语句会获取脚本函数listOnTimeout,index参数值设置的是常量0和JS中的kOnTimeout保持一致

      v8::Local<v8::Value> cb_v = object()->Get(index);
      CHECK(cb_v->IsFunction());
      return listOnTimeoutMakeCallback(cb_v.As<v8::Function>(), argc, argv);

uv__run_timers执行时检查是否触发时间,如果时间到了,调用listOnTimeout脚本函数执行回调函数

immediate对象关联

uv__run_check -> CheckImmediate -> MakeCallback(node.cc)

JS中

    function createImmediate(args, callback) {
      // declaring it `const immediate` causes v6.0.0 to deoptimize this function
      var immediate = new Immediate();
      immediate._callback = callback;
      immediate._argv = args;
      immediate._onImmediate = callback;

      if (!process._needImmediateCallback) {
        process._needImmediateCallback = true;                //这里在process中设置一些函数状态
        process._immediateCallback = processImmediate;        //在c中会在进行对应
      }

      immediateQueue.append(immediate);

      return immediate;
    }

uv__run_check->CheckImmediate->MakeCallback(node.cc)

    static void CheckImmediate(uv_check_t* handle) {
      Environment* env = Environment::from_immediate_check_handle(handle);
      HandleScope scope(env->isolate());
      Context::Scope context_scope(env->context());
      MakeCallback(env->isolate(),
                   env->process_object(),
                   env->immediate_callback_string(),
                   0,
                   nullptr,
                   {0, 0}).ToLocalChecked();
    }

uv__run_check调用CheckImmediate,最终调用MakeCallback执行processImmediate脚本函数

processNextTick对象关联

JS中

      // Used to run V8's micro task queue.
      var _runMicrotasks = {};

      // *Must* match Environment::TickInfo::Fields in src/env.h.
      var kIndex = 0;
      var kLength = 1;

      process.nextTick = nextTick;
      // Needs to be accessible from beyond this scope.
      process._tickCallback = _tickCallback;
      process._tickDomainCallback = _tickDomainCallback;
C中tick_callback_function函数设置到env上
      Local<Object> process_object = env->process_object();

      Local<String> tick_callback_function_key = env->tick_domain_cb_string();
      Local<Function> tick_callback_function =
          process_object->Get(tick_callback_function_key).As<Function>();

      if (!tick_callback_function->IsFunction()) {
        fprintf(stderr, "process._tickDomainCallback assigned to non-function\n");
        ABORT();
	  }

	  process_object->Set(env->tick_callback_string(), tick_callback_function);
	  env->set_tick_callback_function(tick_callback_function);
processNextTick的调用相对复杂,它并非是直接在LOOP中调用,而是

setTimeout/setInterval执行后会立即触发一次调用

		MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
											  int argc,
											  Local<Value>* argv) {
		  CHECK(env()->context() == env()->isolate()->GetCurrentContext());

		  //......................

		  Local<Object> process = env()->process_object();

		  if (tick_info->length() == 0) {
		  tick_info->set_index(0);
		  return ret;
		  }
		  //------------这句-------------//
		  MaybeLocal<Value> rcheck =
			env()->tick_callback_function()->Call(env()->context(),
												  process,
												  0,
												  nullptr);

		  // Make sure the stack unwound properly.
		  CHECK_EQ(env()->current_async_id(), 0);
		  CHECK_EQ(env()->trigger_id(), 0);

		  return rcheck.IsEmpty() ? MaybeLocal<Value>() : ret;
		}

setImmediate执行后会立即触发一次调用

		MaybeLocal<Value> MakeCallback(Environment* env,
									  Local<Value> recv,
									  const Local<Function> callback,
									  int argc,
									  Local<Value> argv[],
									  async_context asyncContext) {
		// If you hit this assertion, you forgot to enter the v8::Context first.
		CHECK_EQ(env->context(), env->isolate()->GetCurrentContext());

		//..................

		Environment::TickInfo* tick_info = env->tick_info();

		if (tick_info->length() == 0) {
		  env->isolate()->RunMicrotasks();
		}

		// Make sure the stack unwound properly. If there are nested MakeCallback's
		// then it should return early and not reach this code.
		CHECK_EQ(env->current_async_id(), asyncContext.async_id);
		CHECK_EQ(env->trigger_id(), asyncContext.trigger_async_id);

		Local<Object> process = env->process_object();

		if (tick_info->length() == 0) {
		  tick_info->set_index(0);
		  return ret;
		}
		//------------这句-------------//
		if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
		  return Undefined(env->isolate());
		}

		return ret;
		}

有人把process.nextTick归结为microtask,其实不是那么回事,这里确实有runmicrotask,但那个是和process_next 文件里面的schedule函数调度microtask有关系,当tickquene空的时候会塞一个调度microtask的任务进去,这样tickquene调用起来的时候microtask也可以执行了。

Note

这里只是讨论了正常情况,在一些异常,超限回调的时候情况又会不一样。值得注意的是首次脚本调用时,LOOP 循环尚未开始,因此也会产生令人疑惑的问题。比如:

	setTimeout(function(){
	  console.log("setTimeout")
	})

	setImmediate(function(){
	  console.log('setImmediate')
	})
这里执行会出现两种结果:
	setImmediate
	setTimeout
	
	//或者
	
	setTimeout
	setImmediate

每次执行的结果都可能不同,前者容易理解,uv_run_time处于LOOP的顶层,自然会早些执行。而后者这种情况就有点奇怪了。分析之后发现原因在于第一次执行脚本文件的过程在node中定义为LoadEnvironment,此时start loop的函数尚未走到。

	void LoadEnvironment(Environment* env) {

	  //............................

	  // Execute the lib/internal/bootstrap_node.js file which was included as a
	  // static C string in node_natives.h by node_js2c.
	  // 'internal_bootstrap_node_native' is the string containing that source code.
	  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
														"bootstrap_node.js");
	  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
	  if (try_catch.HasCaught())  {
		ReportException(env, try_catch);
		exit(10);
	  }

	  //............................

	  // Expose the global object as a property on itself
	  // (Allows you to set stuff on `global` from anywhere in JavaScript.)
	  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

	  // Now we call 'f' with the 'process' variable that we've built up with
	  // all our bindings. Inside bootstrap_node.js and internal/process we'll
	  // take care of assigning things to their places.

	  // We start the process this way in order to be more modular. Developers
	  // who do not like how bootstrap_node.js sets up the module system but do
	  // like Node's I/O bindings may want to replace 'f' with their own function.
	  Local<Value> arg = env->process_object();
	  f->Call(Null(env->isolate()), 1, &arg);
	}

MainSource函数会读取js文件内容,这里可以还可以看到熟悉的global对象。f->Call语句执行脚本,这里同时也会调用NextTick。此时LOOP尚未开始,因而在这两个timer进队列后,timer创建的时间和LOOP update_time的时间可能相同也可能不同,如果程序执行的比较快,时间会相同。那么uv_run_time 检查时间的时候就会跳过去,这次执行就推迟到下一次循环。就会出现这种情况。

验证代码:

	void uv__run_timers(uv_loop_t* loop) {
	  struct heap_node* heap_node;
	  uv_timer_t* handle;

	  for (;;) {
		heap_node = heap_min((struct heap*) &loop->timer_heap);
		if (heap_node == NULL)
		  break;

		handle = container_of(heap_node, uv_timer_t, heap_node);
		if (handle->timeout > loop->time)
		{
		  printf("so fast");
		  break;
		}

		uv_timer_stop(handle);
		uv_timer_again(handle);
		handle->timer_cb(handle);
	  }
	}

测试结果

test result

从结果中可以看出so fast出现的情况下immedate都在timeout之前。进一步推开来看,很多代码也有类似的问题:

//eg
setTimeout(function() {   // setImmediate(function() {
    setTimeout(function() {
        console.log("setTimeout")
    })

    setImmediate(function() {
        console.log("setImmediate")
    })
})

总结一下,timeout,interval的执行时间大体是按照loop的顺序来执行,nextTcik触发执行。但是要完全确定其执行顺序,还需要结合代码分析。设计代码的总体原则是要尽量避免可能存在的block,让任务能够平均,稳定的跑下去。

回到顶部