JavaScript 的单线程
此外,JS 最初是为了解决⽹⻚交互的问题⽽诞⽣的,⽽⽹⻚交互的需求⼤部分是基于⽤户事件的,⽐如点击按钮、输⼊⽂本等。这些操作的响应速度要求很⾼,如果在响应事件的同时还要处理其他任务,可能会导致⽹⻚卡顿、响应变慢等⽤户体验不佳的问题。
为了利⽤多核 CPU 的计算能⼒,HTML5 提出 Web Worker 标准,允许 JS 脚本创建多个线程,但是⼦线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JS 单线程的本质。
线程与进程的区别
1. 进程(Process)
- 定义:进程是一个程序在其运行过程中分配到的资源的集合。每个运行的程序都有自己的进程。
- 特点:
- 独立性:进程是系统中资源分配和管理的基本单位。每个进程拥有独立的地址空间,进程之间是互相独立的。
- 资源消耗大:启动一个新进程需要分配系统资源,如内存、CPU等,代价较大。
- 隔离性强:进程之间不能直接共享内存,需要通过进程间通信(IPC)来交换数据。
2. 线程(Thread)
- 定义:线程是进程中的一个执行单元,它是比进程更小的单位,属于进程的一部分。
- 特点:
- 共享进程资源:同一个进程中的多个线程共享进程的内存和资源(如堆内存、全局变量等)。
- 切换成本低:线程的创建、切换和销毁比进程快,因为线程间共享进程的资源,不需要重新分配资源。
- 轻量级:相比进程,线程的资源消耗较少,系统切换线程的开销较小。
JavaScript中的线程与进程
JavaScript 是一种单线程的语言,这意味着它只能在一个时间点执行一个任务。理解 JavaScript 中的单线程模型需要结合事件循环(Event Loop)和异步任务的概念。
1. 单线程模型
- 主线程:JavaScript 的运行依赖于一个主线程,它负责执行所有的同步代码。由于它是单线程的,这意味着在任何时刻,JavaScript 只能处理一个任务。
- 事件队列:当遇到异步任务时,JavaScript 会将这些任务根据环境的不同交给浏览器或 Node.js 环境来处理,并将回调函数放入事件队列中。当主线程空闲时,事件循环机制会从队列中取出任务并执行。
2. 异步与事件循环
JavaScript 通过事件循环来处理异步操作,像 setTimeout
、Promise
、I/O 操作
等。
- 示例:
console.log("Start");
setTimeout(() => {
console.log("This is an async task");
}, 1000);
console.log("End");
在这个例子中,虽然 setTimeout
指定了 1 秒后执行的任务,但由于 JavaScript 是单线程的,它不会阻塞主线程继续执行后面的代码。setTimeout
任务会被放入事件队列中,当主线程执行完所有同步代码后,事件循环会检查队列,发现有任务需要执行,然后才执行它。
3. Web Workers(多线程)
虽然 JavaScript 本身是单线程的,但通过 Web Workers,可以在浏览器中创建额外的线程,用于处理一些计算密集型任务。
比如在大文件上传过程中的 hash 计算部分,由于需要读取所有文件,依次将每一个分片计算 hash ,是 CPU 密集型应用,直接在主线程操作会造成页面卡顿,所以需要开启额外的 线程来计算 hash 。
-
Web Workers 的特点:
- 运行在与主线程不同的上下文中,不能直接访问 DOM。
- 通常用于处理大规模计算或需要长时间运行的任务。
- 主线程和 Web Worker 通过消息传递来进行通信。
-
Web Worker 示例:
// main.js
const worker = new Worker('worker.js');
worker.postMessage('Start heavy task');
worker.onmessage = (event) => {
console.log('Received from worker:', event.data);
};
// worker.js
onmessage = (event) => {
console.log('Worker received:', event.data);
// 模拟计算密集型任务
let result = 0;
for (let i = 0; i < 1e9; i++) {
result += i;
}
postMessage(result);
};
在这个例子中,main.js
创建了一个 Web Worker 来处理计算密集型任务。Worker 在线程中独立运行,不会阻塞主线程的 UI 渲染或其他操作。
4. Node.js 中的线程与进程
Node.js 是基于 JavaScript 的服务器端环境,虽然 JavaScript 是单线程的,但 Node.js 提供了多进程和多线程处理能力:
-
child_process
模块:可以使用多进程来处理多个任务。例如,创建一个子进程来处理大规模的任务,主进程与子进程之间通过 IPC 通信。- 示例:
const { fork } = require('child_process'); const child = fork('child.js'); child.send('Start task'); child.on('message', (message) => { console.log('Message from child:', message); });
-
worker_threads
模块:Node.js 提供了worker_threads
来实现多线程处理复杂的计算任务,但这些线程依然共享同一进程的资源。当操作系统命令 CPU 去执行一个进程时,实际上是该进程的多个线程之间切换执行,主要是为了充分利用多核 CPU(比如线程数 === CPU 数,线程永不阻塞,没有 io,只存在大量运算)。- 示例:
const { Worker, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); worker.on('message', (message) => { console.log('Received from worker:', message); }); } else { let result = 0; for (let i = 0; i < 1e9; i++) { result += i; } parentPort.postMessage(result); }
这个示例展示了如何在 Node.js 中使用 worker_threads
来执行计算密集型任务而不阻塞主线程。
浏览器的多进程架构
“JS 是单线程的” 指的是执⾏ JS 的线程只有⼀个,是浏览器提供的 JS 引擎线程(主线程)。
如今的主流浏览器都是多进程架构的,以 Chrome 为例,它包含了 1 个浏览器主进程、1个 GPU 进程、1 个⽹络进程、多个渲染进程或多个插件进程。默认情况下,Chrome 会为每个 Tab 标签创建⼀个渲染进程。⼀个渲染进程通常由以下线程组成:
- JS 引擎线程(主线程):
JavaScript 引擎,也称为 JS 内核,负责处理 JS 脚本,执⾏代码。当主线程空闲且任务队列不为空时,会依次取出任务执⾏。注意,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执⾏ JS 时间过⻓,将导致⻚⾯渲染的阻塞。 - GUI 渲染线程:
主要负责⻚⾯的渲染,解析 HTML、CSS,构建DOM树,布局和绘制等。当界⾯需要重绘或者由于某种操作引发重排时,将执⾏该线程。注意:该线程与 JS 引擎线程互斥,当执⾏ JS 引擎线程时,GUI 线程会被挂起,当任务队列空闲时,主线程才会去执⾏ GUI 渲染。 - 事件触发线程:
⽤于控制事件循环,将准备好的事件交给 JS 引擎线程执⾏。当主线程遇到异步任务,如 setTimeOut(或 ajax 请求、⿏标点击事件),会将它们交由对应的线程处理,处理完毕后,事件触发线程会把对应的事件添加到任务队列的尾部,等待 JS 引擎的处理。注意:由于 JS 的单线程关系,队列中的待处理事件都得排队等待,只有在 JS 引擎空闲时才能被执⾏。 - 定时器触发线程:
负责执⾏定时器⼀类函数的线程,如 setTimeout,setInterval 等。主线程依次执⾏代码时,遇到定时器,会将定时器交由该线程进⾏计时,当计时结束,事件触发线程会将定时器的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执⾏。 - 异步 http 请求线程:
负责执⾏异步请求⼀类的函数的线程,如 Promise,axios,ajax 等。主线程依次执⾏代码时,遇到异步请求,会将函数交给该线程处理。当监听到状态码变更,如果设置有回调函数,事件触发线程会将相应的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执⾏。