Node 的进程和线程
# 进程是操作系统的基本概念
- 计算机的核心是 CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。(CPU)
- 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。
- 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。(进程)
- 一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。(线程)
- 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。(内存)
- 每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所:这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(单个)
- 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。(单个,措施)
- 有些房间,可以同时容纳 n 个人,比如厨房。也就是说,如果人数大于 n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。(多个)
- 就是在门口挂 n 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。(多个,措施)
# 操作系统的设计,因此可以归结为三点
- (1)以多进程形式,允许多个任务同时运行;
- (2)以多线程形式,允许单个任务分成不同的部分运行;
- (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
# 进程和线程的区别和关系
- 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
# 多进程和多线程
- 多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。
- 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
# 浏览器多进程架构
跟现在的很多多线程浏览器不一样,Chrome 浏览器使用多个进程来隔离不同的网页。因此在 Chrome 中打开一个网页相当于起了一个进程
# 那么 Chrome 为什么要使用多进程架构?
在浏览器刚被设计出来的时候,那时的网页非常的简单,每个网页的资源占有率是非常低的,因此一个进程处理多个网页时可行的。然后在今天,大量网页变得日益复杂。把所有网页都放进一个进程的浏览器面临在健壮性,响应速度,安全性方面的挑战。因为如果浏览器中的一个 tab 网页崩溃的话,将会导致其他被打开的网页应用。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。
在了解这个知识点线,我们需要先说明下什么是浏览器内核。
# 浏览器内核
简单来说浏览器内核是通过取得页面内容、整理信息(应用 CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。从上面我们可以知道,Chrome 浏览器为每个 tab 页面单独启用进程,因此每个 tab 网页都有由其独立的渲染引擎实例。
# 浏览器内核是多线程
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步
一个浏览器通常由以下常驻线程组成
- JavaScript 引擎线程
- 定时触发器线程
- 事件触发线程
- 异步 http 请求线程
- GUI 渲染线程
- GUI 渲染线程
GUI 渲染线程负责渲染浏览器界面 HTML 元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在 Javascript 引擎运行脚本期间,GUI 渲染线程都是处于挂起状态的,也就是说被”冻结”了.
- Javascript 引擎线程 Javascript 引擎,也可以称为 JS 内核,主要负责处理 Javascript 脚本程序,例如 V8 引擎。Javascript 引擎线程理所当然是负责解析 Javascript 脚本,运行代码。
Javascript 是单线程的, 那么为什么 Javascript 要是单线程的?
这是因为 Javascript 这门脚本语言诞生的使命所致:JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突; 如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。
GUI 渲染线程 与 JavaScript 引擎线程互斥!
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系,当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
JS 阻塞页面加载
从上面我们可以推理出,由于 GUI 渲染线程与 JavaScript 执行线程是互斥的关系,当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
定时触发器线程 浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
事件触发线程 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。
异步 http 请求线程 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。
# Node 中的进程和线程
# Node.js 中的进程 Process
- Node.js 中的进程 Process 是一个全局对象,无需 require 直接使用
- 多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。
- Node.js 在 v0.8 版本之后新增了 Cluster 来实现多进程架构) ,即 多进程 + 单线程 模式。
- 开启多进程不是为了解决高并发,主要是解决了单进程模式下 Node.js CPU 利用率不足的情况,充分利用多核 CPU 的性能。
# Node 中进程的各种 API
- process.env:环境变量,通过 process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:node 的微任务
- process.pid:获取当前进程 id
- process.ppid:当前进程对应的父进程
- process.cwd():获取当前进程工作目录
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
- 进程事件:process.on('uncaughtException', cb) 捕获异常信息、process.on('exit', cb)进程推出监听
- 三个标准流:process.stdout 标准输出、process.stdin 标准输入、process.stderr 标准错误输出
# Node.js 进程创建(创建子进程)
我们可以使用 Node.js 的 child_process 模块很容易地衍生一个子进程,并且那些父子进程使用一个消息系统相互之间可以很容易地交流。
# 以 child_process 模块为例,四种方式
- child_process.spawn():适用于返回大量数据,例如图像处理,二进制数据处理。
- child_process.exec():适用于小量数据,maxBuffer 默认值为 200 * 1024 超出这个默认值将会导致程序崩溃,数据量过大可采用 spawn。
- child_process.execFile():类似 child_process.exec(),区别是不能通过 shell 来执行,不支持像 I/O 重定向和文件查找这样的行为
- child_process.fork(): 衍生新的进程,进程之间是相互独立的,每个进程都有自己的 V8 实例、内存,系统资源是有限的,不建议衍生太多的子进程出来,通长根据系统CPU 核心数设置。
const {spawn } = require('child_process');
const child = spawn('pwd');
2
# 父子进程之间的通信
- 当父进程手动调用 child.discount 的时候,discount 事件会被触发。
- 如果进程不能被衍生(spawn)或者被 killed,error 事件被触发。
- 当一个子进程的 stdio 关闭的时候,close 事件被触发。
- message 事件是最重要的一个。当子进程使用 process.send() 函数发送信息的时候,message 事件会被触发。
# Nodejs 的线程
Node 是单线程吗?
其实只有 js 执行是单线程,I/O 显然是其它线程
Node 是单线程,但是,Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。
- 主线程:编译、执行代码。
- 编译/优化线程:在主线程执行的时候,可以优化代码。
- 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
- 垃圾回收的几个线程。
node 在 v10 过后提出了 worker_threads 模块,它是在一个单独的 node v8 实例进程里面,可以创建多个线程来进行搞 CPU 任务。
参考: