node 的事件循环

280 阅读7分钟

本文将结合朴灵的《深入检出Node.js》以及部分node源码讲解一下node的事件循环。

在 node 的官方文档里面,把node的事件循环分成下面几个部分。首先这几个大的部分是没有问题的,但是如果大家按照官方描述的去理解,肯定与现实情况有出入,要想真正了解node的事件循环,建议看一下《深入检出Node.js》,然后结合着node的源码进行分析。

  • 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测:setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。

接下来我们以 fs.open() 为例,看一下这个阶段都发生了什么

1. fs.open()

  • 调用 lib/fs.js 里面的 open,这儿会创建一个很重要的req,后面会根据这个req创建变量对象
function open(path, flags, mode, callback) {
  ...
  // 创建req,会根据这个变量创建请求对象
  const req = new FSReqCallback();

  // 将回调函数绑定在 onComplete 上面
  req.oncomplete = callback;

  // 调用 c/c++ 源码
  binding.open(pathModule.toNamespacedPath(path),
               flagsNumber,
               mode,
               req);
}
  • 调用 src/node_file.cc 下面的 open(),首先判断环境,然后创建一个请求对象,根据不同的环境调用不同的 uv_fs_open 方法
static void Open(const FunctionCallbackInfo<Value>& args) {
  // 环境判断
  Environment* env = Environment::GetCurrent(args);
  ...
  // 根据创建的 req 生成一个请求对象
  FSReqBase* req_wrap_async = GetReqWrap(args, 3);

  // 异步调用,调用对应平台下面的 uv_fs_open 方法
  if (req_wrap_async != nullptr) {  // open(path, flags, mode, req)
    AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
              uv_fs_open, *path, flags, mode);

  } else {  // open(path, flags, mode, undefined, ctx)
    CHECK_EQ(argc, 5);
    FSReqWrapSync req_wrap_sync;
    FS_SYNC_TRACE_BEGIN(open);
    int result = SyncCall(env, args[4], &req_wrap_sync, "open",
                          uv_fs_open, *path, flags, mode);
    FS_SYNC_TRACE_END(open);
    ...
  }
}
  • 如果是windows,就调用 deps/uv/src/win/fs.c 下面的 uv_fs_open()
int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags,
    int mode, uv_fs_cb cb) {
  int err;

  INIT(UV_FS_OPEN);
  err = fs__capture_path(req, path, NULL, cb != NULL);
  if (err) {
    SET_REQ_WIN32_ERROR(req, err);
    return req->result;
  }

  req->fs.info.file_flags = flags;
  req->fs.info.mode = mode;
  POST;
}
  • 将请求对象(这个请求对象是存放回调函数,执行结果的重要媒介)放进线程池中等待
  • 当线程中有可用线程时,调用响应的底层函数。
  • js调用立即返回

2. 线程池做的工作

  • 当线程可用时;(科补一下进程与线程的关系,最开始只有进程,因为进程的释放开销太大,又引入了线程,线程是进程的子集,进程主要用来占用一定资源,线程来跑程序)
  • 执行请求对象中的I/O操作
  • 将结果放到请求对象中
  • 通知IOCP(这个是window的异步I/O模型,Unix对应的是自定义线程池)调用完成 -> 这个状态可以通过事件循环中的 GetQueuedCompletionStatus() 进行获取
  • 归还线程

其实就是在处理完成的时候,通知IOCP调用完成,而整个过程,事件循环都在不停的获取状态是否已经完成

3. 事件循环

3.1. uv_run()

node事件循环的核心就是 uv_run() 函数,这个函数的内容如下

int uv_run(uv_loop_t *loop, uv_run_mode mode) {
  DWORD 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); // 更新 loop 里面的时间
    uv__run_timers(loop); // timer阶段根据 loop 里面的时间与回调的timeout进行比较,如果已经超时就执行回调函数

    ran_pending = uv_process_reqs(loop); // pending阶段,将在这里面消耗 loop 中生成的回调队列
    uv_idle_invoke(loop); // idle阶段
    uv_prepare_invoke(loop); // prepare阶段

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      // 取出下一个定时器的过期时间与当前loop时间的时间差 `diff = handle->timeout - loop->time;`
      timeout = uv_backend_timeout(loop);

    // 这儿其实是一个性能的选择
    if (pGetQueuedCompletionStatusEx)
      uv__poll(loop, timeout); // poll阶段
    else
      uv__poll_wine(loop, timeout);


    uv_check_invoke(loop); // check阶段
    uv_process_endgames(loop); // 关闭的回调函数阶段,loop里面得到的队列将在这里面执行
    // 如果模式是只执行一次
    if (mode == UV_RUN_ONCE) {
      uv__run_timers(loop);
    }
    // 再次判断事件循环是否活着
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

我们可以看到所谓的事件循环就是一个while(),不停的循环,依次此次执行每个阶段,这儿要注意的就是 poll 阶段。接下来我们分别看一下每个阶段

3.2 这个是定时器阶段

主要调用了 uv__run_timers(loop) ,做了如下操作:依次取出事件堆里的最小的定时器,如果已经过期了,就马上执行对应的回调;如果没有过期或者定时器已经取完了,那么就跳出循环

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

  for (;;) {
    // 取出时间堆里面最小的一个
    heap_node = heap_min(timer_heap(loop));
    // 如果时间堆里面没有内容了,说明定时器的回调已经执行完了
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    // 因为定时器没有过期就直接跳出
    if (handle->timeout > loop->time)
      break;
    
    // 移除定时器
    uv_timer_stop(handle);
    // 针对 setInterval 的情况
    uv_timer_again(handle);
    // 执行对应的回调函数
    handle->timer_cb(handle);
  }
}

3.3 pending阶段

调用了uv_process_reqs(loop) 这个函数的目的就是消耗 poll 产生的队列

INLINE static int uv_process_reqs(uv_loop_t* loop) {
  uv_req_t* req;
  uv_req_t* first;
  uv_req_t* next;

  if (loop->pending_reqs_tail == NULL)
    return 0;
  // pending_reqs_tail 队列就是 poll 阶段产生的队列(但是绝不仅是 poll 阶段产生的,还包括其他操作产生的一些内容也会在这里面)
  // 取出队列中的第一个元素
  first = loop->pending_reqs_tail->next_req;
  next = first;
  loop->pending_reqs_tail = NULL;

  while (next != NULL) {
    req = next;
    next = req->next_req != first ? req->next_req : NULL;
    // 根据请求对象不同的 type 执行不同的内容
    switch (req->type) {
      case UV_READ:
        DELEGATE_STREAM_REQ(loop, req, read, data);
        break;
		...
      case UV_POLL_REQ:
        uv_process_poll_req(loop, (uv_poll_t*) req->data, req);
        break;
      default:
        assert(0);
    }
  }

  return 1;
}

这儿的 uv_process_poll_req 函数很重要,poll阶段产生的回调都将在这儿执行

void uv_process_poll_req(uv_loop_t* loop, uv_poll_t* handle, uv_req_t* req) {
  if (!(handle->flags & UV_HANDLE_POLL_SLOW)) {
    uv__fast_poll_process_poll_req(loop, handle, req);
  } else {
    uv__slow_poll_process_poll_req(loop, handle, req);
  }
}

3.4 轮询阶段

uv__poll(loop, timeout)

  • 轮询阶段是一个 for 循环,
  • 首先是通过 pGetQueuedCompletionStatusEx/GetQueuedCompletionStatus 获取 iocp 里面是否有 io 完成的状态,
  • 如果有,那么就遍历将所有的请求对象都添加到一个队列里面,然后结束loop阶段
  • 如果没有,就会检查传进来的timeout是否大于0,然后判断计算的过期时间(timeout_time = loop->time + timeout)是否大于当前的loop时间, 如果大于,那么更新timeout,然后continue
  • 也就是如果还有定时器,那么loop循环将在最小的定时器有过期时间的时候结束整个循环
static void uv__poll(uv_loop_t* loop, DWORD timeout) {
  BOOL success;
  uv_req_t* req;
  OVERLAPPED_ENTRY overlappeds[128];
  ULONG count;
  ULONG i;
  int repeat;
  uint64_t timeout_time;
  // 重新计算得出过期时间(当前loop时间 + diff时间)
  timeout_time = loop->time + timeout;

  // 这儿就是一直循环,直到有满足退出条件的时候退出
  for (repeat = 0; ; repeat++) {
    // 查看iocp里面是否有io完成的状态
    success = pGetQueuedCompletionStatusEx(loop->iocp,
                                           overlappeds,
                                           ARRAY_SIZE(overlappeds),
                                           &count,
                                           timeout,
                                           FALSE);
    // 如果有完成的
    if (success) {
      for (i = 0; i < count; i++) {
        if (overlappeds[i].lpOverlapped) {
          req = uv_overlapped_to_req(overlappeds[i].lpOverlapped);
          uv_insert_pending_req(loop, req); // 插入到一个队列里面 pending_reqs_tail ,在里面没有执行回调的逻辑
        }
      }
      uv_update_time(loop); // 更新loop的时间
    } else if (GetLastError() != WAIT_TIMEOUT) {
      /* Serious error */
      uv_fatal_error(GetLastError(), "GetQueuedCompletionStatusEx");
    } else if (timeout > 0) {
      uv_update_time(loop);
      // 如果没有到过期时间,那么就continue
      if (timeout_time > loop->time) {
        timeout = (DWORD)(timeout_time - loop->time);
        timeout += repeat ? (1 << (repeat - 1)) : 0;
        continue;
      }
    }
    // 结束 loop
    break;
  }
}
  • 轮询阶段 uv__poll_wine(loop, timeout); 这个方法和 uv__poll 的区别在于,查看是否有完成状态的方式用的是 GetQueuedCompletionStatus ,同时一次也就读出一个。

3.5 check 阶段

调用了uv_check_invoke(loop),主要执行 setImmediate 里面的回调

void uv_check_invoke(uv_loop_t* loop) {
    uv_check_t* handle;
    (loop)->next_check_handle = (loop)->name##_handles;
    // 遍历 check_handle 链表,然后执行回调,这里面的回调都是
    // 在调用 setImmediate 执行 uv_check_init()/uv_check_start 时加入进去的
    while ((loop)->next_check_handle != NULL) {
      handle = (loop)->next_check_handle;
      (loop)->next_check_handle = handle->name##_next;
      handle->name##_cb(handle);
    }
  }
  • 我们看一下 setImmediate 的定义,这里面会在 uv_check_init 加入回调
void* SetImmediate(napi_env env, T&& cb) {
  T* ptr = new T(std::move(cb));
  uv_loop_t* loop = nullptr;
  uv_check_t* check = new uv_check_t;
  check->data = ptr;
  NAPI_ASSERT(env,
              napi_get_uv_event_loop(env, &loop) == napi_ok,
              "can get event loop");

  uv_check_init(loop, check); // 对父类进行初始化,同时将回调相关放到一个 handle_queue 队列里面
  uv_check_start(check, [](uv_check_t* check) { // 增加一些标记,计数器等等

  ...
  return nullptr;
}

3.6 关闭的回调函数阶段

调用了uv_process_endgames(loop),主要做一些重置操作,代码如下

INLINE static void uv_process_endgames(uv_loop_t* loop) {
  uv_handle_t* handle;
  // endgame_handles 是一个队列
  while (loop->endgame_handles) {
    // 获取队列中的第一个
    handle = loop->endgame_handles;
    // 指向队列中的下一个
    loop->endgame_handles = handle->endgame_next;

    handle->flags &= ~UV_HANDLE_ENDGAME_QUEUED;
    // 根据不同的type,执行不同的函数
    switch (handle->type) {
      ...
      case UV_POLL:
        // 这儿是poll相关
        uv_poll_endgame(loop, (uv_poll_t*) handle);
        break;
      case UV_TIMER:
        uv__timer_close((uv_timer_t*) handle); // 移除对里面的定时器
        uv__handle_close(handle); // 执行结束回调
        break;
        ...
      default:
        assert(0);
        break;
    }
  }
}