- 进程是由操作系统产生的
- 进程可以被视为程序的一个运行实例
- 操作系统负责给应用程序(即:进程)分配和管理资源
- 每个进程运行在其自己独立的地址空间
- 一个应用程序包含一个或多个进程
- 一个进程包含一个或多个线程
- 当操作系统去启动应用程序的时候,会给应用程序分配一个进程
- 每个进程都拥有独立的、可伸缩的内存空间
- 进程和进程之间,原则上是相互隔离的,互不影响的,但必要的时候,它们也可以进行通信
- 一个应用程序在启动时只有一个进程,但它在运行的过程中,可以开启新的进程,进程之间仍然保持相对独立
- 如果一个进程是直接由操作系统开启,则它叫做主进程
- 主进程 vs 子进程
- 主进程:当操作系统启动应用程序时,它会为该应用程序创建一个新的进程。这个进程通常被称为“主进程”或“父进程”。这是应用程序的初始和主要的运行环境。
- 子进程:主进程可以创建其他进程,这些进程通常被称为“子进程”。子进程从它们的父进程继承资源和环境设置(例如环境变量),但它们有自己独立的执行流和地址空间。
- 如果一个进程B是由进程A开启,则A是B的父进程,B是A的子进程,子进程会继承父进程的一些信息,但仍然保持相对独立。子进程发生的任何错误,不会对父进程产生任何影响,它们的内存空间是完全隔离的
- 你可以认为主进程是应用程序的“起始点”或“根”,而子进程是从主进程派生出来,执行特定的任务或功能的进程。
- 本节介绍的重点是线程而非进程
- 打开 chrome 浏览器的命令:
- win:
chrome
- mac:
open "/applicaitons/google chrome.app"
- win:
- 操作系统每启动一个进程(无论是主进程,还是子进程),都会自动为它分配一个线程,称之为主线程
- 线程是进程内的执行单元,负责执行进程中的代码。
- 问:程序是在进程上运行还是在线程上运行?答:程序运行在线程上,但由于线程位于进程中,所以回答程序运行在进程上也正确。
- 主线程在运行的过程中,可以创建多个线程,这些线程称之为子线程
- 一个进程往往有多个线程
- 当操作系统命令 CPU 去执行一个进程时,实际上,是在该进程的多个线程中切换执行
- 线程和进程很相似,它们都是独立运行,最大的区别在于:线程的内存空间没有隔离,共享进程的内存空间
- 由于一个进程下的所有线程都共享这个进程的内存空间,因此线程之间的数据不用遵守任何协议,可以随意使用
- 在线程之间切换的时候,一个函数的执行可能会被打断
- 使用线程的主要目的是为了充分使用多核 CPU
- 在使用线程时,理论上最优的场景是:
- 一个 CPU 核心运行一个线程
- 线程永不阻塞
- 没有 IO
- 只存在大量运算
- 线程相对独立,几乎不使用进程提供的共享数据
- 线程并非越多越好,随着线程数量的增加,线程之间来回切换的开支也会增加
- IO 操作
- 程序耗时的主要部分集中在 IO 操作,耗时普遍占 90%↑
- 问:什么情况下线程不会阻塞?答:没有 IO 操作,只存在大量运算
- CPU 密集型 vs IO 密集型
- 线程适合用于处理 CPU 密集型操作(运算操作)
- 线程不适合用于处理 IO 密集型操作
- IO 密集型操作适合使用异步
child_process
- 使用 Node.js 的
child_process
模块,你可以在 Node.js 环境中创建和管理子进程 child_process
可以让你从 Node.js 进程中运行其他系统命令或其他 Node.js 脚本,这样你就可以并行执行多个操作
- 使用 Node.js 的
require('child_process').exec(<命令字符串>, <回调函数>)
exec
是一个用于执行 shell 命令的方法- 参数 1:一个字符串形式的命令
- 参数 2:回调函数
const childProcess = require("child_process"); // 导入内置模块
childProcess.exec(`xxx`, (error, stdout, stdErr) => {
// 当你使用 exec 方法时,Node.js 会为你执行的命令 xxx 创建一个新的 shell 进程
// 在这个新的进程中,命令会被执行,然后当命令执行完毕后,子进程将结果返回给主 Node.js 进程
//
// 回调函数中可以获取子进程的标准输出,这种数据交互是通过 IPC 完成的,nodejs 已经帮你完成了处理
// error:开启进程过程中发生的错误(例如命令不存在或命令执行失败)
// stdout: 子进程输出的正常内容
// stdErr: 子进程输出的错误内容
});
const childProcess = require("child_process")
childProcess.exec("ls", (error, stdout, stderr) => {
if (error) {
console.error(`exec error:${error}`)
return
}
console.log('stdout: ${stdout}`);
})
- 任何有效的
**shell**
命令,都可以丢给**childProcess.exec**
执行 - mac 环境,使用
shell
命令:- 查看当前目录下的文件:
ls
- 打开 chrome 浏览器:
open "/applications/google chrome.app"
- 打开微信:
open "/applications/wechat.app"
- ……
- 查看当前目录下的文件:
const {
exec
} = require("child_process")
exec("open '/applications/wechat.app'") // 打开微信
exec("open '/applications/google chrome.app'") // 打开 chrome 浏览器
- 过去,nodejs 没有提供给用户创建线程的接口,只能使用进程的方式
- 过去,nodejs 还提供了 cluster 模块,可以通过另一种模式来创建进程
- 现在,nodejs 已经提供了线程模块,对进程的操作已经很少使用了
- Node.js 中的
worker_threads
模块,允许你在 Node.js 中管理线程 - 你可以使用
**worker_threads**
模块在不影响主线程的情况下执行 CPU 密集型任务,从而提高性能和并发性 - 使用 nodejs 新建子线程实例:
new Worker(<子线程文件所在位置>, <配置>)
- 主线程往子线程传递数据:在新建子线程的时候,通过
workerData
选项来传递数据给子线程 - 子线程接收父线程传递的数据:
require("worker_threads").workerData
- 子线程往父线程选地消息:
require("worker_threads").parentPort.postMessage(data)
- 父线程监听子线程的消息:
worker.on("message", (data) => { ... })
- 在父线程中终止一个子线程:
worker.terminate()
// isPrime.js
module.exports = function (n) {
// 素数:只能被 1 和自身整除的数
// 用暴力算法来实现,以便测试线程处理 CPU 密集型任务的耗时
if (n < 2) {
return false;
}
for (let i = 2; i < n; i++) {
if (n % i === 0) {
return false;
}
}
return true;
};
// index.js
const {
Worker
} = require("worker_threads");
const path = require("path");
const testArr = new Array(100).fill(0).map((_, i) => i + 1)
// Array.from({ length: 100 }, (_, i) => i + 1)
// 新建子线程实例
const worker = new Worker(path.resolve(__dirname, "worker.js"), {
workerData: testArr,
});
worker.on("exit", () => {
console.log("子线程退出了");
});
worker.on('message', (...args) => {
console.log('received msg from worker.js => ', args)
});
// worker.js
const isPrime = require("./isPrime");
const {
parentPort, // 用于与父线程通信的端口
workerData, // 获取线程启动时传递的数据
threadId, // 获取线程的唯一编号
} = require("worker_threads");
const name = `线程 ${threadId}`;
const newArr = [];
console.log('received workerData from main thread => ', workerData)
for (let i = 0; i < workerData.length; i++) {
const n = workerData[i];
if (isPrime(n)) {
newArr.push(n);
}
}
console.log(`${name}处理完成,并把结果给予了主线程`);
parentPort.postMessage(newArr);
- demo 通过子线程充分利用 CPU 多核来处理 CPU 密集型任务
测试数据:numbers.json
// isPrime.js
module.exports = function (n) {
// 素数:只能被 1 和自身整除的数
// 用暴力算法来实现,以便测试线程处理 CPU 密集型任务的耗时
if (n < 2) {
return false;
}
for (let i = 2; i < n; i++) {
if (n % i === 0) {
return false;
}
}
return true;
};
// index.js
const {
Worker
} = require("worker_threads")
const {
cpus
} = require("os")
const arr = require("./numbers.json")
const cpuNum = cpus().length
const len = Math.ceil(arr.length / cpuNum) // 得到每个线程需要处理的数字数量
const newArr = [] // 保存最终结果
let n = cpuNum // 正在处理数据的线程数量
console.time('处理耗时')
for (let i = 0; i < cpuNum; i++) {
const data = arr.slice(i * len, (i + 1) * len)
const worker = new Worker("./worker.js", {
workerData: data,
})
worker.on("message", (result) => {
newArr.push(...result)
n--
if (n === 0) {
console.timeEnd('处理耗时')
// 所有线程都处理结束
console.log(newArr) // 输出最终结果
}
worker.terminate()
})
}
// worker.js
const isPrime = require("./isPrime");
const {
parentPort, // 用于与父线程通信的端口
workerData, // 获取线程启动时传递的数据
threadId, // 获取线程的唯一编号
} = require("worker_threads");
const name = `线程 ${threadId}`;
const newArr = [];
for (let i = 0; i < workerData.length; i++) {
const n = workerData[i];
if (isPrime(n)) {
newArr.push(n);
}
}
console.log(`${name} 处理完成,并把结果给予了主线程`);
parentPort.postMessage(newArr);
一个 CPU 核心运行一个线程,充分利用 CPU 计算资源,耗时:709.961ms
// index.js
const {
Worker
} = require("worker_threads")
const arr = require("./numbers.json")
console.time('处理耗时')
// 新建子线程实例
const worker = new Worker("./worker.js", {
workerData: arr,
})
worker.on('message', (data) => {
console.log('received msg from worker.js => ', data)
worker.terminate()
})
worker.on("exit", () => {
console.log("子线程退出了")
console.timeEnd('处理耗时')
})
所有数据都交给一个线程去完成,耗时:2.252s