Node.js进程模型

Node.js进程模型

Posted by nolan on September 15, 2022

Node.js 内核的幕后英雄 — 子线程

image

libuv 线程池

最为人所知的子线程。Libuv 用来处理文件 IO、DNS 和 CPU 密集型的任务。

image

以异步读取文件为例:

image

主线程提交任务给线程池后,就可以继续执行其他操作而不需要等待子线程完成,子线程完成任务后会以异步的方式通知主线程。

Workers

利用 node.js 提供的 worker_threads 模块创建的子线程。

V8 Platform 线程池

Node.js 在初始化时会创建一个 WorkerThreadsTaskRunner,在这里完成 gc 任务等;

WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(int thread_pool_size) {
  Mutex platform_workers_mutex;
  ConditionVariable platform_workers_ready;

  Mutex::ScopedLock lock(platform_workers_mutex);
  int pending_platform_workers = thread_pool_size;

  delayed_task_scheduler_ = std::make_unique<DelayedTaskScheduler>(
      &pending_worker_tasks_);
  // threads_ 是线程队列
  threads_.push_back(delayed_task_scheduler_->Start());

  for (int i = 0; i < thread_pool_size; i++) {
    PlatformWorkerData* worker_data = new PlatformWorkerData{
      &pending_worker_tasks_, &platform_workers_mutex,
      &platform_workers_ready, &pending_platform_workers, i
    };
    std::unique_ptr<uv_thread_t> t { new uv_thread_t() };
    // 创建线程
    if (uv_thread_create(t.get(), PlatformWorkerThread,
                         worker_data) != 0) {
      break;
    }
    threads_.push_back(std::move(t));
  }
}

首先会根据 thread_pool_size 创建多个子线程。还创建了一个延迟任务的调度器 DelayedTaskScheduler,这个对象里也创建了一个线程。

std::unique_ptr<uv_thread_t> Start() {
    auto start_thread = [](void* data) {
      // 处理任务
      static_cast<DelayedTaskScheduler*>(data)->Run();
    };
    std::unique_ptr<uv_thread_t> t { new uv_thread_t() };
    uv_sem_init(&ready_, 0);
    CHECK_EQ(0, uv_thread_create(t.get(), start_thread, this));
    uv_sem_wait(&ready_);
    uv_sem_destroy(&ready_);
    return t;
  }

Node.js 初始化后就创建了一波线程,然后 V8 会使用这些线程,我们通过 Post 和 PostDelayedTask 可以看到。

image

inspector

Node.js 会创建一个新的线程运行调试相关的代码。

void InspectorIo::ThreadMain() {
  uv_loop_t loop;
  loop.data = nullptr;
  int err = uv_loop_init(&loop);
  // 获取调试的 host 和端口
  std::string host;
  int port;
  {
    ExclusiveAccess<HostPort>::Scoped host_port(host_port_);
    host = host_port->host();
    port = host_port->port();
  }
  // 创建服务器,往事件循环注册 fd
  InspectorSocketServer server(std::move(delegate),
                               &loop,
                               std::move(host),
                               port,
                               inspect_publish_uid_);
  server.Start();
  // 启动事件循环
  uv_run(&loop, UV_RUN_DEFAULT);
  CheckedUvLoopClose(&loop);
}

开了子线程避免了主线程阻塞时所带来的限制。如果是单线程,主线程阻塞了,调试也无法进行。

Trace

Trace 功能是 Node 10 版本引入的功能,用来追踪 node 的一些性能。这个功能也用到了子线程。

void NodeTraceWriter::InitializeOnThread(uv_loop_t* loop) {
  tracing_loop_ = loop;

  flush_signal_.data = this;
  int err = uv_async_init(tracing_loop_, &flush_signal_,
                          [](uv_async_t* signal) {
    NodeTraceWriter* trace_writer =
        ContainerOf(&NodeTraceWriter::flush_signal_, signal);
    trace_writer->FlushPrivate();
  });
  CHECK_EQ(err, 0);

  exit_signal_.data = this;
  err = uv_async_init(tracing_loop_, &exit_signal_, ExitSignalCb);
}

watchDog

看门狗是计算机中的一个术语,大概就是定时做一些事情的一个程序,比如启动一个定时器定时检测系统是否运行正常,如果系统运行正常,则在定时器超时前重置定时器,如果系统挂了,则看门狗就会发出告警。

watchDog 是 vm 模块的,vm 模块可以执行传入的 js 代码,watchDog 用来设置监控执行时间的,超时就结束执行。

在主线程里执行一段 js 代码,如果死循环了就卡死了,这个 watchDog 就是开一个线程,监控他,超时就终止他的执行,这样不会导致死循环。