EventLoop 事件循环

"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘


  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration.
  • idle, prepare: only used internally.
  • poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
  • check: setImmediate() callbacks are invoked here. close callbacks: some close callbacks, e.g. socket.on('close', ...).

事件循环

事件循环的职责,就是不断得等待事件的发生,然后将这个事件的所有处理器,以它们订阅这个事件的时间顺序,依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。

当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件处理器是一个一个按顺序执行的。

即如果某个事件绑定了两个处理器,那么第二个处理器会在第一个处理器执行完毕后,才开始执行。在这个事件的所有处理器都执行完毕之前,事件循环不会去检查是否有新的事件触发。在单个线程中,一切都是有顺序地一个一个地执行的!

Node.js 中的事件循环

Node采用V8作为JavaScript的执行引擎,同时使用libuv实现事件驱动式异步I/O。其事件循环就是采用了libuv的默认事件循环。 在src/node.cc中,

Environment* env = CreateEnvironment(
        node_isolate,
        uv_default_loop(),
        context,
        argc,
        argv,
        exec_argc,
        exec_argv);

这段代码建立了一个node执行环境,可以看到第三行的uv_default_loop(),这是libuv库中的一个函数,它会初始化uv库本身以及其中的default_loop_struct,并返回一个指向它的指针default_loop_ptr。 之后,Node会载入执行环境并完成一些设置操作,然后启动event loop:

bool more;
do {
  more = uv_run(env->event_loop(), UV_RUN_ONCE);
  if (more == false) {
    EmitBeforeExit(env);
    // Emit `beforeExit` if the loop became alive either after emitting
    // event, or after running some callbacks.
    more = uv_loop_alive(env->event_loop());
    if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
      more = true;
  }
} while (more == true);
code = EmitExit(env);
RunAtExit(env);

more用来标识是否进行下一轮循环。 env->event_loop()会返回之前保存在env中的default_loop_ptr,uv_run函数将以指定的UV_RUN_ONCE模式启动libuv的event loop。在这种模式下,uv_run会至少处理一个事件:这意味着,如果当前事件队列中没有需要处理的I/O事件,uv_run会阻塞住,直到有I/O事件需要处理,或者下一个定时器时间到。如果当前没有I/O事件也没有定时器事件,则uv_run返回false。

接下来Node会根据more的情况决定下一步操作:

  • 如果more为true,则继续运行下一轮loop。

  • 如果more为false,说明已经没有等待处理的事件了,EmitBeforeExit(env);触发进程的'beforeExit'事件,检查并处理相应的处理函数,完成后直接跳出循环。

最后触发'exit'事件,执行相应的回调函数,Node运行结束,后面会进行一些资源释放操作。

在libuv中,event loop会在每次循环的开始更新自己的time从而实现计时功能,而I/O事件则分为两类:

  • Network I/O是使用系统提供的非阻塞式I/O解决方案,例如在Linux上使用epoll,windows上使用IOCP。

  • 文件操作和DNS操作没有(很好的)系统解决方案,因此libuv自建了线程池,在其中进行阻塞式I/O。

另外我们也可以将自定义的函数抛到线程池中运行,在运行结束后主线程会执行相应的回调函数,不过Node并没有将这一项功能加入到JavaScript中,也就是说只用原生Node是无法在JavaScript中开启新的线程进行并行执行的。

process.nextTick

带着这个问题,我们看看 JS 层的 nextTick 是怎么被驱动。

在入口点 src/node.js, processNextTick 方法构建了 process.nextTick API。

process._tickCallback 作为 nexttick 的回调函数,挂到了 process 对象上,由 C++层面回调使用。

startup.processNextTick = function() {
    var nextTickQueue = [];
    var pendingUnhandledRejections = [];
    var microtasksScheduled = false;

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

    // This tickInfo thing is used so that the C++ code in src/node.cc
    // can have easy access to our nextTick state, and avoid unnecessary
    // calls into JS land.
    const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);
    // 省略...
}

通过 process._setupNextTick 注册 _tickCallbackenvtick_callback_function 上。

src/async_wrap.cc 文件中,我们发现对其的调用如下:

Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
                                      int argc,
                                      Local<Value>* argv) {
  // ...
  Environment::TickInfo* tick_info = env()->tick_info();

  if (tick_info->in_tick()) {
    return ret;
  }

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

  if (tick_info->length() == 0) {
    tick_info->set_index(0);
    return ret;
  }

  tick_info->set_in_tick(true);

  env()->tick_callback_function()->Call(process, 0, nullptr);

  tick_info->set_in_tick(false);
  // ...

当无 nextTick任务时,env()->isolate()->RunMicrotasks();会驱动 Promise 任务执行。

否则会调用 tick_callback_function ,也就是 _tickCallback

看到这里我也有个疑问,如果没有异步 IO 呢,怎么驱动呢?

我们来到 lib/module.js, 如下

// 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();
};

Module._load 加载主脚本后,就调用 _tickCallback, 处理第一次的 tick 了。

所以上面的疑问有了答案,nextTick 主要在 uv__io_poll 驱动。为什么说主要呢? 因为还 可能在 Timer模块驱动,具体细节留给读者去研究啦。

总结

参考

  • http://acemood.github.io/2016/02/01/event-loop-in-javascript/