
目录Node.js 中的进程和线程一、先理解什么是程序二、什么是进程三、生活中的例子四、任务管理器里看到的是什么五、什么是线程六、为什么需要线程七、进程和线程到底是什么关系八、进程和线程的区别九、为什么说进程之间比较“隔离”十、线程为什么比较特殊十一、Node.js 和进程有什么关系十二、Node.js 是单线程的吗十三、Node.js 为什么一个线程也能处理很多请求十四、I/O 密集型和 CPU 密集型1. I/O 密集型2.CPU 密集型十五、Node.js 中的 child_process十六、为什么需要子进程十七、Node.js 中的 Worker Threads十八、child_process 和 Worker Threads 的区别十九、Worker Thread 什么时候有用二十、Node.js 的线程池又是什么二十一、Worker Threads 和 libuv Thread Pool 区别二十二、怎么查看 Node.js 进程二十三、Node.js 自己也可以查看 PID二十四、查看父进程 PID二十五、Windows 查看 Node.js 进程二十六、Windows 查看某个进程的线程二十七、macOS 查看 Node.js 进程二十八、macOS 查看某个进程的线程二十九、一个完整例子三十、一个特别重要的理解三十一、进程、线程、Node.js 可以这样串起来三十二、什么时候使用什么三十三、最后用一句话理解Node.js 中的进程和线程本篇从计算机基础出发理解什么是进程Process、线程Thread以及它们在 Node.js 中到底有什么用。很多人在学习 Node.js 的时候会看到进程 线程 子进程 Worker Threads Cluster 多线程 CPU 密集型任务这些概念看起来很多其实它们背后的核心并不复杂。最重要的是先理解程序 → 进程 → 线程一、先理解什么是程序我们平时写的代码本质上只是一个程序Program。例如console.log(Hello Node.js)我们把它保存成hello.js此时hello.js只是一个文件。它本身还没有“运行起来”。可以简单理解为hello.js ↓ 程序代码 ↓ 还没有执行二、什么是进程当我们执行nodehello.js操作系统就会启动一个Node.js 进程。也就是说进程是程序的一次执行过程。可以简单理解程序 ↓ 运行 ↓ 进程例如nodehello.js操作系统可能产生Node.js 进程 PID 3621这里PID就是Process ID进程 ID。每个正在运行的进程通常都有一个自己的 PID。三、生活中的例子可以把程序理解成菜谱而进程理解成按照这个菜谱正在做菜的一次过程。同一个程序可以同时运行很多次。例如nodeapp.jsnodeapp.jsnodeapp.js虽然运行的是同一个app.js但操作系统可以创建三个不同的进程app.js ↓ ┌───────────────┐ │ Node 进程 #1 │ PID 1001 └───────────────┘ ┌───────────────┐ │ Node 进程 #2 │ PID 1002 └───────────────┘ ┌───────────────┐ │ Node 进程 #3 │ PID 1003 └───────────────┘它们是三个不同的进程。四、任务管理器里看到的是什么在 Windows 中打开任务管理器你会看到Chrome Node.js VS Code 微信这些正在运行的程序背后都对应着操作系统中的一个或多个进程。例如Node.js PID: 3621这里Node.js ↓ 进程 ↓ PID 3621所以我们经常会说“这个 Node 进程的 PID 是 3621。”五、什么是线程有了进程以后再理解线程。线程Thread可以简单理解成进程中的一个执行流。也就是说进程 ├── 线程 ├── 线程 └── 线程一个进程可以拥有一个或者多个线程。六、为什么需要线程假设一个程序要同时做三件事情任务 A读取文件 任务 B计算数据 任务 C处理网络请求如果所有事情都由一个执行流按照顺序执行A ↓ B ↓ C那么一个任务执行时间很长就可能影响后面的任务。线程的意义之一就是让程序拥有多个可以独立执行的执行流。例如进程 │ ├── 线程 1 → 处理任务 A ├── 线程 2 → 处理任务 B └── 线程 3 → 处理任务 C当然实际能否真正同时执行还和 CPU 核心、操作系统调度等因素有关。七、进程和线程到底是什么关系这是最重要的一张图操作系统 │ ├── 进程 A │ │ │ ├── 线程 A1 │ ├── 线程 A2 │ └── 线程 A3 │ ├── 进程 B │ │ │ ├── 线程 B1 │ └── 线程 B2 │ └── 进程 C │ └── 线程 C1所以线程属于进程。线程不能脱离进程独立存在。可以记成进程 一个正在运行的程序实例 线程 进程里面真正执行任务的执行流八、进程和线程的区别对比进程线程英文ProcessThread是什么程序的一次执行过程进程中的执行流是否独立相对独立属于进程资源拥有自己的资源通常共享所属进程资源创建成本通常较高通常较低通信进程间通信线程间通信更方便一个进程支持多个✅✅PID有通常也有对应线程标识九、为什么说进程之间比较“隔离”例如进程 A │ ├── 内存 └── 数据 进程 B │ ├── 内存 └── 数据进程 A 和进程 B 通常拥有相互隔离的地址空间。所以// 进程 Aletname张三并不会因为存在另一个进程 B就直接变成// 进程 Bconsole.log(name)进程之间如果需要交换数据需要使用专门的通信机制。例如IPC Socket Pipe Message十、线程为什么比较特殊同一个进程里的线程通常共享进程的大部分资源。例如Node.js 进程 │ ├── Thread 1 ├── Thread 2 └── Thread 3 │ └── 共享进程资源因此线程之间通信通常比进程之间更方便。但是共享资源也意味着需要考虑并发访问、同步、数据竞争等问题。这也是多线程编程比较复杂的原因之一。十一、Node.js 和进程有什么关系我们平时执行nodeapp.js本质上就是操作系统 ↓ 启动 Node.js ↓ 创建 Node.js 进程 ↓ 执行 app.js例如nodeapp.js可以理解成Node.js 进程 │ ├── 执行 JavaScript ├── 管理内存 ├── 处理事件循环 ├── 处理网络请求 └── 与操作系统交互十二、Node.js 是单线程的吗这是非常容易被误解的地方。很多教程会说Node.js 是单线程的。这句话不够准确。更准确地说Node.js 的 JavaScript 执行通常由一个主线程上的事件循环驱动。也就是说我们写console.log(A)console.log(B)console.log(C)JavaScript 代码默认是按照A ↓ B ↓ C这样的执行流运行。但是Node.js 整个运行时并不是“只有一个线程”。Node.js 底层还会涉及V8 libuv 操作系统 线程池 Worker Threads所以JavaScript 执行模型 ↓ 通常一个主线程不等于整个 Node.js 进程 ↓ 只有一个线程十三、Node.js 为什么一个线程也能处理很多请求这就涉及 Node.js 最重要的概念事件循环Event Loop例如constfsrequire(node:fs)fs.readFile(./test.txt,utf8,(err,data){console.log(data)})console.log(读取文件)很多初学者会想“读取文件是不是把 JavaScript 线程卡住了”通常不是简单地这么理解。Node.js 会把一些 I/O 工作交给底层机制处理。大致可以理解为JavaScript 主线程 │ │ 发起文件读取 ↓ Node.js │ ↓ 操作系统 / libuv │ ↓ 文件读取 │ ↓ 完成 │ ↓ 回调进入事件循环 │ ↓ JavaScript 主线程继续处理所以 Node.js 特别适合HTTP 请求 API 服务 WebSocket 文件 I/OInput / Output 数据库请求 网络 I/OInput / Output这类大量存在等待时间的任务。十四、I/O 密集型和 CPU 密集型理解 Node.js 进程和线程一定要认识这两个词。1. I/O 密集型例如读取数据库 读取文件 发送 HTTP 请求 等待网络特点大量时间在“等待”例如constdataawaitfetch(https://example.com)程序发起请求后需要等待服务器响应。这类任务非常适合 Node.js 的异步 I/O 模型。2.CPU 密集型例如图片处理 视频编码 大量数学计算 机器学习计算 复杂数据处理 大规模 JSON 计算例如functionheavyTask(){letresult0for(leti0;i10_000_000_000;i){resulti}returnresult}这种任务会长时间占用 CPU。如果直接在 Node.js 主 JavaScript 线程中执行heavyTask()那么事件循环就会被阻塞。结果可能是用户请求 A ↓ heavyTask() ↓ CPU 长时间计算 ↓ 事件循环被阻塞 ↓ 用户请求 B 等待 用户请求 C 等待 用户请求 D 等待这就是为什么CPU 密集型任务不能随便丢到 Node.js 主线程执行。十五、Node.js 中的 child_processNode.js 提供了node:child_process可以创建子进程。例如import{exec}fromnode:child_processexec(ls,(error,stdout,stderr){console.log(stdout)})这里可以理解为主 Node.js 进程 │ │ 创建 ↓ 子进程 │ └── 执行 ls子进程是一个独立的操作系统进程。十六、为什么需要子进程假设 Node.js 要调用一个外部程序ffmpeg python git shell 命令 其他可执行程序就可以考虑Node.js │ └── child_process ↓ 外部进程例如import{exec}fromnode:child_processexec(node -v,(error,stdout){console.log(stdout)})Node.js 启动另一个进程执行命令。十七、Node.js 中的 Worker ThreadsNode.js 还提供node:worker_threads用于创建 Worker。例如import{Worker}fromnode:worker_threadsconstworkernewWorker(./worker.js)worker.on(message,message{console.log(收到 Worker 消息,message)})可以简单理解Node.js 主线程 │ │ 创建 ↓ Worker Thread │ └── 执行计算任务这和child_process不一样。十八、child_process 和 Worker Threads 的区别这是 Node.js 面试很容易问到的内容。child_process ↓ 创建子进程 worker_threads ↓ 创建 Worker 线程简单对比child_processworker_threads创建什么进程线程隔离程度更高相对较低是否独立进程是否适合外部程序、独立任务CPU 密集型 JS通信IPC 等postMessage 等资源开销通常更大通常更小十九、Worker Thread 什么时候有用例如主线程 │ ├── HTTP 请求 ├── WebSocket ├── API └── Worker ↓ 大量计算主线程负责处理请求Worker 负责复杂计算这样可以避免长时间的 CPU 计算直接阻塞主事件循环。二十、Node.js 的线程池又是什么Node.js 底层使用了libuvlibuv 中存在线程池机制。一些操作可能交给线程池处理。可以粗略理解Node.js │ ├── JavaScript 主线程 │ ↓ │ Event Loop │ └── libuv ↓ Thread Pool ├── Worker ├── Worker ├── Worker └── Worker注意libuv 的线程池和worker_threads不是一回事。这是初学者非常容易混淆的地方。二十一、Worker Threads 和 libuv Thread Pool 区别简单理解worker_threads ↓ 你主动创建 Worker ↓ 主要用于执行 JavaScript 代码而libuv Thread Pool ↓ Node.js / libuv 底层使用 ↓ 帮助处理部分异步操作所以不要简单认为Node.js 的所有线程 Worker Threads不是这样的。二十二、怎么查看 Node.js 进程首先运行一个 Node.js 程序console.log(PID:,process.pid)setInterval((){},1000)运行nodeapp.js可能看到PID: 3621这时候3621就是当前 Node.js 进程的 PID。二十三、Node.js 自己也可以查看 PIDNode.js 提供console.log(process.pid)例如console.log(当前进程 PID,process.pid)输出当前进程 PID3621所以process.pid ↓ 当前 Node.js 进程的 PID二十四、查看父进程 PIDNode.js 还可以console.log(process.ppid)其中pid ↓ 当前进程 PID ppid ↓ 父进程 PID例如父进程 PID 1000 │ ↓ Node.js PID 3621那么process.pid// 3621process.ppid// 1000二十五、Windows 查看 Node.js 进程如果你使用 Windows可以使用tasklist查看所有进程。也可以tasklist /FI PID eq 3621只查看 PID 为3621的进程。如果安装了 Sysinternals 的 PsList也可以pslist 3621查看指定进程的信息。二十六、Windows 查看某个进程的线程使用 PsListpslist -dmx 3621这里3621 ↓ PID可以查看这个进程相关的详细信息以及线程信息。注意pslist不是 Windows 默认自带的 CMD 命令而是 Sysinternals 工具中的 PsList。二十七、macOS 查看 Node.js 进程macOS 可以使用ps-p3621查看 PID 为3621的进程。例如ps-p3621二十八、macOS 查看某个进程的线程可以ps-M-p3621或者ps-M3621简单记忆ps -p 3621 ↓ 看进程 ps -M -p 3621 ↓ 看线程二十九、一个完整例子我们写一个 Node.js 程序console.log(当前 PID,process.pid)console.log(父进程 PID,process.ppid)setInterval((){console.log(程序正在运行...)},3000)运行nodeapp.js可能得到当前 PID3621 父进程 PID1000此时操作系统 │ ├── 父进程 PID 1000 │ └── Node.js 进程 PID 3621 │ ├── Node.js 主线程 └── 其他底层线程然后在另一个终端ps-p3621可以查看Node.js 进程再执行ps-M-p3621可以进一步查看这个进程的线程三十、一个特别重要的理解不要把Node.js直接理解成只有一个线程更准确的理解应该是Node.js 进程 │ ├── JavaScript 主线程 │ │ │ └── Event Loop │ ├── libuv 相关线程 │ ├── V8 / Node.js 内部线程 │ └── Worker Threads如果主动创建因此Node.js 的 JavaScript 执行模型偏向单线程但 Node.js 运行时本身并不是只有一个线程。三十一、进程、线程、Node.js 可以这样串起来最终可以把整个知识体系记成程序 │ │ 运行 ↓ 进程 Process │ ├── 线程 Thread │ ├── 内存 │ ├── 文件描述符 │ └── 其他资源到了 Node.jsnode app.js │ ↓ Node.js 进程 │ ├── JavaScript 主线程 │ │ │ └── Event Loop │ ├── libuv │ │ │ └── Thread Pool │ └── Worker Threads如果需要独立进程Node.js │ └── child_process ↓ 子进程三十二、什么时候使用什么可以先用下面这张表判断场景常见选择普通 APINode.js Event Loop网络请求异步 I/O数据库查询异步 I/O文件操作Node.js 异步 API调用 Pythonchild_process调用 FFmpegchild_process调用 Shell 命令child_process大量 JS CPU 计算worker_threads多进程运行 Node 服务cluster / 多进程方案三十三、最后用一句话理解如果只记住几个概念程序 一份代码。 进程 这份程序正在运行的一次实例。 线程 进程里面真正执行任务的执行流。 Node.js 通常由一个 JavaScript 主线程 Event Loop 驱动代码执行 但整个 Node.js 运行时并不只有一个线程。 child_process 创建子进程。 worker_threads 创建 Worker 线程。 libuv Thread Pool Node.js 底层用于部分异步工作的线程池。最核心的关系就是程序 ↓ 运行 ↓ 进程 ↓ 拥有 ↓ 线程而 Node.js 则是在这个计算机基础之上构建出了进程 ├── Event Loop ├── JavaScript 主线程 ├── libuv │ └── Thread Pool ├── child_process │ └── 子进程 └── worker_threads └── Worker 线程所以你现在学习的进程和线程本质上确实属于计算机基础。但它非常重要因为后面学习Node.js ↓ Event Loop ↓ 异步 I/O ↓ libuv ↓ Worker Threads ↓ child_process ↓ Cluster ↓ Node.js 性能优化都会不断用到这些概念。