nodejs 进程和线程 - 图1

    1. 进程是由操作系统产生的
    2. 进程可以被视为程序的一个运行实例
    3. 操作系统负责给应用程序(即:进程)分配和管理资源
    4. 每个进程运行在其自己独立的地址空间
    5. 一个应用程序包含一个或多个进程
    6. 一个进程包含一个或多个线程
    7. 当操作系统去启动应用程序的时候,会给应用程序分配一个进程
    8. 每个进程都拥有独立的、可伸缩的内存空间
    9. 进程和进程之间,原则上是相互隔离的,互不影响的,但必要的时候,它们也可以进行通信
    10. 一个应用程序在启动时只有一个进程,但它在运行的过程中,可以开启新的进程,进程之间仍然保持相对独立
    11. 如果一个进程是直接由操作系统开启,则它叫做主进程
    12. 主进程 vs 子进程
      1. 主进程:当操作系统启动应用程序时,它会为该应用程序创建一个新的进程。这个进程通常被称为“主进程”或“父进程”。这是应用程序的初始和主要的运行环境。
      2. 子进程:主进程可以创建其他进程,这些进程通常被称为“子进程”。子进程从它们的父进程继承资源和环境设置(例如环境变量),但它们有自己独立的执行流和地址空间。
      3. 如果一个进程B是由进程A开启,则A是B的父进程,B是A的子进程,子进程会继承父进程的一些信息,但仍然保持相对独立。子进程发生的任何错误,不会对父进程产生任何影响,它们的内存空间是完全隔离的
      4. 你可以认为主进程是应用程序的“起始点”或“根”,而子进程是从主进程派生出来,执行特定的任务或功能的进程。
    13. 本节介绍的重点是线程而非进程
    14. 打开 chrome 浏览器的命令:
      1. win:chrome
      2. mac:open "/applicaitons/google chrome.app"
    15. 操作系统每启动一个进程(无论是主进程,还是子进程),都会自动为它分配一个线程,称之为主线程
    16. 线程是进程内的执行单元,负责执行进程中的代码。
    17. 问:程序是在进程上运行还是在线程上运行?答:程序运行在线程上,但由于线程位于进程中,所以回答程序运行在进程上也正确。
    18. 主线程在运行的过程中,可以创建多个线程,这些线程称之为子线程
    19. 一个进程往往有多个线程
    20. 当操作系统命令 CPU 去执行一个进程时,实际上,是在该进程的多个线程中切换执行
    21. 线程和进程很相似,它们都是独立运行,最大的区别在于:线程的内存空间没有隔离,共享进程的内存空间
    22. 由于一个进程下的所有线程都共享这个进程的内存空间,因此线程之间的数据不用遵守任何协议,可以随意使用
    23. 在线程之间切换的时候,一个函数的执行可能会被打断
    24. 使用线程的主要目的是为了充分使用多核 CPU
    25. 在使用线程时,理论上最优的场景是:
      1. 一个 CPU 核心运行一个线程
      2. 线程永不阻塞
        1. 没有 IO
        2. 只存在大量运算
      3. 线程相对独立,几乎不使用进程提供的共享数据
    26. 线程并非越多越好,随着线程数量的增加,线程之间来回切换的开支也会增加
    27. IO 操作
      1. 程序耗时的主要部分集中在 IO 操作,耗时普遍占 90%↑
      2. 问:什么情况下线程不会阻塞?答:没有 IO 操作,只存在大量运算
    28. CPU 密集型 vs IO 密集型
      1. 线程适合用于处理 CPU 密集型操作(运算操作)
      2. 线程不适合用于处理 IO 密集型操作
      3. IO 密集型操作适合使用异步
    29. child_process
      1. 使用 Node.js 的 child_process 模块,你可以在 Node.js 环境中创建和管理子进程
      2. child_process 可以让你从 Node.js 进程中运行其他系统命令或其他 Node.js 脚本,这样你就可以并行执行多个操作
    30. require('child_process').exec(<命令字符串>, <回调函数>)
      1. exec是一个用于执行 shell 命令的方法
      2. 参数 1:一个字符串形式的命令
      3. 参数 2:回调函数
    1. const childProcess = require("child_process"); // 导入内置模块
    2. childProcess.exec(`xxx`, (error, stdout, stdErr) => {
    3. // 当你使用 exec 方法时,Node.js 会为你执行的命令 xxx 创建一个新的 shell 进程
    4. // 在这个新的进程中,命令会被执行,然后当命令执行完毕后,子进程将结果返回给主 Node.js 进程
    5. //
    6. // 回调函数中可以获取子进程的标准输出,这种数据交互是通过 IPC 完成的,nodejs 已经帮你完成了处理
    7. // error:开启进程过程中发生的错误(例如命令不存在或命令执行失败)
    8. // stdout: 子进程输出的正常内容
    9. // stdErr: 子进程输出的错误内容
    10. });
    1. const childProcess = require("child_process")
    2. childProcess.exec("ls", (error, stdout, stderr) => {
    3. if (error) {
    4. console.error(`exec error:${error}`)
    5. return
    6. }
    7. console.log('stdout: ${stdout}`);
    8. })
    1. 任何有效的 **shell** 命令,都可以丢给 **childProcess.exec** 执行
    2. mac 环境,使用 shell 命令:
      1. 查看当前目录下的文件:ls
      2. 打开 chrome 浏览器:open "/applications/google chrome.app"
      3. 打开微信:open "/applications/wechat.app"
      4. ……
    1. const {
    2. exec
    3. } = require("child_process")
    4. exec("open '/applications/wechat.app'") // 打开微信
    5. exec("open '/applications/google chrome.app'") // 打开 chrome 浏览器
    1. 过去,nodejs 没有提供给用户创建线程的接口,只能使用进程的方式
    2. 过去,nodejs 还提供了 cluster 模块,可以通过另一种模式来创建进程
    3. 现在,nodejs 已经提供了线程模块,对进程的操作已经很少使用了
    4. Node.js 中的 worker_threads 模块,允许你在 Node.js 中管理线程
    5. 你可以使用 **worker_threads** 模块在不影响主线程的情况下执行 CPU 密集型任务,从而提高性能和并发性
    6. 使用 nodejs 新建子线程实例:new Worker(<子线程文件所在位置>, <配置>)
    7. 主线程往子线程传递数据:在新建子线程的时候,通过 workerData 选项来传递数据给子线程
    8. 子线程接收父线程传递的数据:require("worker_threads").workerData
    9. 子线程往父线程选地消息:require("worker_threads").parentPort.postMessage(data)
    10. 父线程监听子线程的消息:worker.on("message", (data) => { ... })
    11. 在父线程中终止一个子线程:worker.terminate()

    nodejs 进程和线程 - 图2

    1. // isPrime.js
    2. module.exports = function (n) {
    3. // 素数:只能被 1 和自身整除的数
    4. // 用暴力算法来实现,以便测试线程处理 CPU 密集型任务的耗时
    5. if (n < 2) {
    6. return false;
    7. }
    8. for (let i = 2; i < n; i++) {
    9. if (n % i === 0) {
    10. return false;
    11. }
    12. }
    13. return true;
    14. };
    1. // index.js
    2. const {
    3. Worker
    4. } = require("worker_threads");
    5. const path = require("path");
    6. const testArr = new Array(100).fill(0).map((_, i) => i + 1)
    7. // Array.from({ length: 100 }, (_, i) => i + 1)
    8. // 新建子线程实例
    9. const worker = new Worker(path.resolve(__dirname, "worker.js"), {
    10. workerData: testArr,
    11. });
    12. worker.on("exit", () => {
    13. console.log("子线程退出了");
    14. });
    15. worker.on('message', (...args) => {
    16. console.log('received msg from worker.js => ', args)
    17. });
    1. // worker.js
    2. const isPrime = require("./isPrime");
    3. const {
    4. parentPort, // 用于与父线程通信的端口
    5. workerData, // 获取线程启动时传递的数据
    6. threadId, // 获取线程的唯一编号
    7. } = require("worker_threads");
    8. const name = `线程 ${threadId}`;
    9. const newArr = [];
    10. console.log('received workerData from main thread => ', workerData)
    11. for (let i = 0; i < workerData.length; i++) {
    12. const n = workerData[i];
    13. if (isPrime(n)) {
    14. newArr.push(n);
    15. }
    16. }
    17. console.log(`${name}处理完成,并把结果给予了主线程`);
    18. parentPort.postMessage(newArr);

    image.png

    1. demo 通过子线程充分利用 CPU 多核来处理 CPU 密集型任务

    测试数据:numbers.json

    1. // isPrime.js
    2. module.exports = function (n) {
    3. // 素数:只能被 1 和自身整除的数
    4. // 用暴力算法来实现,以便测试线程处理 CPU 密集型任务的耗时
    5. if (n < 2) {
    6. return false;
    7. }
    8. for (let i = 2; i < n; i++) {
    9. if (n % i === 0) {
    10. return false;
    11. }
    12. }
    13. return true;
    14. };
    1. // index.js
    2. const {
    3. Worker
    4. } = require("worker_threads")
    5. const {
    6. cpus
    7. } = require("os")
    8. const arr = require("./numbers.json")
    9. const cpuNum = cpus().length
    10. const len = Math.ceil(arr.length / cpuNum) // 得到每个线程需要处理的数字数量
    11. const newArr = [] // 保存最终结果
    12. let n = cpuNum // 正在处理数据的线程数量
    13. console.time('处理耗时')
    14. for (let i = 0; i < cpuNum; i++) {
    15. const data = arr.slice(i * len, (i + 1) * len)
    16. const worker = new Worker("./worker.js", {
    17. workerData: data,
    18. })
    19. worker.on("message", (result) => {
    20. newArr.push(...result)
    21. n--
    22. if (n === 0) {
    23. console.timeEnd('处理耗时')
    24. // 所有线程都处理结束
    25. console.log(newArr) // 输出最终结果
    26. }
    27. worker.terminate()
    28. })
    29. }
    1. // worker.js
    2. const isPrime = require("./isPrime");
    3. const {
    4. parentPort, // 用于与父线程通信的端口
    5. workerData, // 获取线程启动时传递的数据
    6. threadId, // 获取线程的唯一编号
    7. } = require("worker_threads");
    8. const name = `线程 ${threadId}`;
    9. const newArr = [];
    10. for (let i = 0; i < workerData.length; i++) {
    11. const n = workerData[i];
    12. if (isPrime(n)) {
    13. newArr.push(n);
    14. }
    15. }
    16. console.log(`${name} 处理完成,并把结果给予了主线程`);
    17. parentPort.postMessage(newArr);

    一个 CPU 核心运行一个线程,充分利用 CPU 计算资源,耗时:709.961ms

    image.png

    1. // index.js
    2. const {
    3. Worker
    4. } = require("worker_threads")
    5. const arr = require("./numbers.json")
    6. console.time('处理耗时')
    7. // 新建子线程实例
    8. const worker = new Worker("./worker.js", {
    9. workerData: arr,
    10. })
    11. worker.on('message', (data) => {
    12. console.log('received msg from worker.js => ', data)
    13. worker.terminate()
    14. })
    15. worker.on("exit", () => {
    16. console.log("子线程退出了")
    17. console.timeEnd('处理耗时')
    18. })

    所有数据都交给一个线程去完成,耗时:2.252s

    image.png