目錄
文章導覽
#執行緒
單執行緒
單執行緒的一些說明
Node.js 中的程序與執行緒
Node.js 中的進程
process 模組
Node.js 程式建立
child_process模組
fork开启子进程 Demo
cluster模块
cluster原理分析
child_process 模块与cluster 模块总结
Node.js进程通信原理
Node.js句柄传递
子进程对象send()方法可以发送的句柄类型
send句柄发送原理分析
Node.js多进程架构模型
Node.js 进程守护
什么是进程守护?
如何实现进程守护
linux 关闭一个进程
Node.js 线程
Node.js关于单线程的误区
某些异步 IO 会占用额外的线程
Libuv
Node.js 线程创建
worker_thread 模块
总结
首頁 web前端 js教程 深入淺析Node.js中的進程和執行緒

深入淺析Node.js中的進程和執行緒

May 11, 2021 am 11:03 AM
node.js 執行緒 行程

本篇文章帶大家深入了解Node.js中的進程和執行緒。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

深入淺析Node.js中的進程和執行緒

進程線程是程式設計師的必知概念,面試常被問到,但是有些文章內容只是講講理論知識,可能有些小夥伴並沒有真的理解,在實際開發中應用也比較少。本篇文章除了介紹概念,透過Node.js 的角度講解進程線程,並且講解一些在專案中的實戰的應用,讓你不僅能迎戰面試官還可以在實戰中完美應用。 【推薦學習:《nodejs 教學》】

文章導覽

深入淺析Node.js中的進程和執行緒

##面試會問

Node.js是單執行緒嗎?

Node.js 做耗時的運算時候,如何避免阻塞?

Node.js如何實作多進程的開啟與關閉?

Node.js可以建立執行緒嗎?

你們開發過程中如何實現程式守護的?

除了使用第三方模組,你們自己是否封裝過一個多進程架構?

進程

#行程

Process是電腦中的程式關於某資料集合上的運行活動,是系統進行資源分配與調度的基本單位,是作業系統結構的基礎,行程是執行緒的容器(來自百科) 。進程是資源分配的最小單位。我們啟動一個服務、運行一個實例,就是開一個服務進程,例如Java 裡的JVM 本身就是一個進程,Node.js 裡透過node app.js 開啟一個服務進程,多進程就是進程的複製(fork),fork 出來的每個進程都擁有自己的獨立空間位址、資料棧,一個進程無法存取另外一個進程裡定義的變數、資料結構,只有建立了IPC 通信,進程之間才可資料共享。

    Node.js開啟服務進程範例
  • const http = require('http');
    
    const server = http.createServer();
    server.listen(3000,()=>{
        process.title='程序员成长指北测试进程';
        console.log('进程id',process.pid)
    })
    登入後複製
執行上面程式碼後,以下為Mac 系統自帶的監控工具「活動監視器」 所展示的效果,可以看到我們剛開啟的Nodejs 行程7663

深入淺析Node.js中的進程和執行緒

#執行緒

執行緒是作業系統能夠進行運算調度的最小單位,首先我們要清楚執行緒是隸屬於進程的,被包含在進程之中。

一個執行緒只能隸屬於一個行程,但是一個行程是可以擁有多個執行緒的

單執行緒

單執行緒就是一個行程只開一個執行緒

Javascript 就是屬於單執行緒,程式順序執行(這裡暫且不提JS異步),可以想像一下隊列,前面一個執行完之後,後面才可以執行,當你在使用單線程語言編碼時切勿有過多耗時的同步操作,否則線程會造成阻塞,導致後續響應無法處理。你如果採用 Javascript 進行程式設計時候,請盡可能的利用Javascript非同步操作的特性。

經典計算耗時造成執行緒阻塞的範例
const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on(&#39;request&#39;, (req, res) => {
  if (req.url === &#39;/compute&#39;) {
    console.info(&#39;计算开始&#39;,new Date());
    const sum = longComputation();
    console.info(&#39;计算结束&#39;,new Date());
    return res.end(`Sum is ${sum}`);
  } else {
    res.end(&#39;Ok&#39;)
  }
});

server.listen(3000);
//打印结果
//计算开始 2019-07-28T07:08:49.849Z
//计算结束 2019-07-28T07:09:04.522Z
登入後複製

查看列印結果,當我們呼叫

127.0.0.1:3000/compute的時候,如果想要呼叫其他的路由位址例如127.0.0.1/大約需要15秒時間,也可以說一個使用者請求完第一個compute介面後需要等待15秒,這對使用者來說是極度不友善的。下文我會透過建立多進程的方式child_process.forkcluster 來解決這個問題。

單執行緒的一些說明

  • Node.js 雖然是單執行緒模型,但其基於事件驅動、非同步非阻塞模式,可以應用於高並發場景,避免了執行緒建立、執行緒之間上下文切換所產生的資源開銷。
  • 當你的專案中需要有大量運算,CPU 耗時的操作時候,要注意考慮開啟多進程來完成了。
  • Node.js 開發過程中,錯誤會造成整個應用程式退出,應用的健全性值得考驗,尤其是錯誤的異常拋出,以及進程守護是必須要做的。
  • 單執行緒無法利用多核心CPU,但是後來Node.js 提供的API以及一些第三方工具相應都得到了解決,文章後面都會講到。

Node.js 中的程序與執行緒

Node.js 是Javascript 在服務端的運作環境,建構在chrome 的V8 引擎之上,基於事件驅動、非阻塞I/O模型,充分利用作業系統提供的非同步I/O 進行多任務的執行,適合I/O 密集型的應用場景,因為非同步,程式無需阻塞等待結果返回,而是基於回呼通知的機制,原本同步模式等待的時間,則可以用來處理其它任務,

科普:在Web 伺服器方面,著名的Nginx 也是採用此模式(事件驅動),避免了多執行緒的執行緒建立、執行緒上下文切換的開銷,Nginx 採用C 語言進行編寫,主要用來做高效能的Web 伺服器,不適合做業務。

Web業務開發中,如果你有高並發應用程式場景那麼 Node.js 會是你不錯的選擇。

在單核心 CPU 系統之上我們採用 單一進程 單執行緒 的模式來開發。在多核心 CPU 系統之上,可以透過 child_process.fork 開啟多個進程(Node.js 在 v0.8 版本之後新增了Cluster 來實現多進程架構) ,即 多進程 單線程 模式。注意:開啟多進程不是為了解決高並發,主要是解決了單進程模式下 Node.js CPU 使用率不足的情況,充分利用多核心 CPU 的效能。

Node.js 中的進程

process 模組

Node.js 中的進程Process 是一個全域對象,無需require 直接使用,給我們提供了當前進程中的相關資訊。官方文件提供了詳細的說明,有興趣的可以親自實踐下 Process 文件。

  • process.env:環境變量,例如透過 process.env.NODE_ENV 取得不同環境項目設定資訊
  • # process.nextTick:這個在談及Event Loop 時常為會提到
  • 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 標準錯誤輸出
  • #process.title 指定進程名稱,有的時候需要給進程指定一個名稱

#以上僅列舉了部分常用到功能點,除了Process 之外Node.js 還提供了child_process模組用來對子程序進行操作,在下文Nodejs進程創建會繼續講述。

Node.js 程式建立

程式建立有多種方式,本篇文章以child_process模組和cluster模組進行解說。

child_process模組

child_process 是Node.js 的內建模組,官網位址:

child_process 官網位址:http://nodejs .cn/api/child_process.html#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 核心数**设置。

CPU 核心数这里特别说明下,fork 确实可以开启多个进程,但是并不建议衍生出来太多的进程,cpu核心数的获取方式const cpus = require(&#39;os&#39;).cpus();,这里 cpus 返回一个对象数组,包含所安装的每个 CPU/内核的信息,二者总和的数组哦。假设主机装有两个cpu,每个cpu有4个核,那么总核数就是8。

fork开启子进程 Demo

fork开启子进程解决文章起初的计算耗时造成线程阻塞。 在进行 compute 计算时创建子进程,子进程计算完成通过 send 方法将结果发送给主进程,主进程通过 message 监听到信息后处理并退出。

fork_app.js

const http = require(&#39;http&#39;);
const fork = require(&#39;child_process&#39;).fork;

const server = http.createServer((req, res) => {
    if(req.url == &#39;/compute&#39;){
        const compute = fork(&#39;./fork_compute.js&#39;);
        compute.send(&#39;开启一个新的子进程&#39;);

        // 当一个子进程使用 process.send() 发送消息时会触发 &#39;message&#39; 事件
        compute.on(&#39;message&#39;, sum => {
            res.end(`Sum is ${sum}`);
            compute.kill();
        });

        // 子进程监听到一些错误消息退出
        compute.on(&#39;close&#39;, (code, signal) => {
            console.log(`收到close事件,子进程收到信号 ${signal} 而终止,退出码 ${code}`);
            compute.kill();
        })
    }else{
        res.end(`ok`);
    }
});
server.listen(3000, 127.0.0.1, () => {
    console.log(`server started at http://${127.0.0.1}:${3000}`);
});
登入後複製

fork_compute.js

针对文初需要进行计算的的例子我们创建子进程拆分出来单独进行运算。

const computation = () => {
    let sum = 0;
    console.info(&#39;计算开始&#39;);
    console.time(&#39;计算耗时&#39;);

    for (let i = 0; i < 1e10; i++) {
        sum += i
    };

    console.info(&#39;计算结束&#39;);
    console.timeEnd(&#39;计算耗时&#39;);
    return sum;
};

process.on(&#39;message&#39;, msg => {
    console.log(msg, &#39;process.pid&#39;, process.pid); // 子进程id
    const sum = computation();

    // 如果Node.js进程是通过进程间通信产生的,那么,process.send()方法可以用来给父进程发送消息
    process.send(sum);
})
登入後複製
cluster模块

cluster 开启子进程Demo

const http = require(&#39;http&#39;);
const numCPUs = require(&#39;os&#39;).cpus().length;
const cluster = require(&#39;cluster&#39;);
if(cluster.isMaster){
    console.log(&#39;Master proces id is&#39;,process.pid);
    // fork workers
    for(let i= 0;i<numCPUs;i++){
        cluster.fork();
    }
    cluster.on(&#39;exit&#39;,function(worker,code,signal){
        console.log(&#39;worker process died,id&#39;,worker.process.pid)
    })
}else{
    // Worker可以共享同一个TCP连接
    // 这里是一个http服务器
    http.createServer(function(req,res){
        res.writeHead(200);
        res.end(&#39;hello word&#39;);
    }).listen(8000);

}
登入後複製
cluster原理分析

深入淺析Node.js中的進程和執行緒

cluster模块调用fork方法来创建子进程,该方法与child_process中的fork是同一个方法。 cluster模块采用的是经典的主从模型,Cluster会创建一个master,然后根据你指定的数量复制出多个子进程,可以使用cluster.isMaster属性判断当前进程是master还是worker(工作进程)。由master进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。

cluster模块使用内置的负载均衡来更好地处理线程之间的压力,该负载均衡使用了Round-robin算法(也被称之为循环算法)。当使用Round-robin调度策略时,master accepts()所有传入的连接请求,然后将相应的TCP请求处理发送给选中的工作进程(该方式仍然通过IPC来进行通信)。

开启多进程时候端口疑问讲解:如果多个Node进程监听同一个端口时会出现 Error:listen EADDRIUNS的错误,而cluster模块为什么可以让多个子进程监听同一个端口呢?原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket具柄发送给子进程。

child_process 模块与cluster 模块总结

无论是 child_process 模块还是 cluster 模块,为了解决 Node.js 实例单线程运行,无法利用多核 CPU 的问题而出现的。核心就是父进程(即 master 进程)负责监听端口,接收到新的请求后将其分发给下面的 worker 进程

cluster模块的一个弊端:

深入淺析Node.js中的進程和執行緒

深入淺析Node.js中的進程和執行緒

cluster内部隐时的构建TCP服务器的方式来说对使用者确实简单和透明了很多,但是这种方式无法像使用child_process那样灵活,因为一直主进程只能管理一组相同的工作进程,而自行通过child_process来创建工作进程,一个主进程可以控制多组进程。原因是child_process操作子进程时,可以隐式的创建多个TCP服务器,对比上面的两幅图应该能理解我说的内容。

Node.js进程通信原理

前面讲解的无论是child_process模块,还是cluster模块,都需要主进程和工作进程之间的通信。通过fork()或者其他API,创建了子进程之后,为了实现父子进程之间的通信,父子进程之间才能通过message和send()传递信息。

IPC这个词我想大家并不陌生,不管那一张开发语言只要提到进程通信,都会提到它。IPC的全称是Inter-Process Communication,即进程间通信。它的目的是为了让不同的进程能够互相访问资源并进行协调工作。实现进程间通信的技术有很多,如命名管道,匿名管道,socket,信号量,共享内存,消息队列等。Node中实现IPC通道是依赖于libuv。windows下由命名管道(name pipe)实现,*nix系统则采用Unix Domain Socket实现。表现在应用层上的进程间通信只有简单的message事件和send()方法,接口十分简洁和消息化。

IPC创建和实现示意图

深入淺析Node.js中的進程和執行緒

IPC通信管道是如何创建的

深入淺析Node.js中的進程和執行緒

父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正的创建出子进程,这个过程中也会通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。

Node.js句柄传递

讲句柄之前,先想一个问题,send句柄发送的时候,真的是将服务器对象发送给了子进程?

子进程对象send()方法可以发送的句柄类型
  • net.Socket TCP套接字
  • net.Server TCP服务器,任意建立在TCP服务上的应用层服务都可以享受它带来的好处
  • net.Native C++层面的TCP套接字或IPC管道
  • dgram.Socket UDP套接字
  • dgram.Native C++层面的UDP套接字
send句柄发送原理分析

结合句柄的发送与还原示意图更容易理解。

深入淺析Node.js中的進程和執行緒

send()方法在将消息发送到IPC管道前,实际将消息组装成了两个对象,一个参数是hadler,另一个是message。message参数如下所示:

{
    cmd:&#39;NODE_HANDLE&#39;,
    type:&#39;net.Server&#39;,
    msg:message
}
登入後複製

发送到IPC管道中的实际上是我们要发送的句柄文件描述符。这个message对象在写入到IPC管道时,也会通过JSON.stringfy()进行序列化。所以最终发送到IPC通道中的信息都是字符串,send()方法能发送消息和句柄并不意味着它能发送任何对象。

连接了IPC通道的子线程可以读取父进程发来的消息,将字符串通过JSON.parse()解析还原为对象后,才触发message事件将消息传递给应用层使用。在这个过程中,消息对象还要被进行过滤处理,message.cmd的值如果以NODE_为前缀,它将响应一个内部事件internalMessage,如果message.cmd值为NODE_HANDLE,它将取出message.type值和得到的文件描述符一起还原出一个对应的对象。

以发送的TCP服务器句柄为例,子进程收到消息后的还原过程代码如下:

function(message,handle,emit){
    var self = this;
    
    var server = new net.Server();
    server.listen(handler,function(){
      emit(server);
    });
}
登入後複製

这段还原代码,子进程根据message.type创建对应的TCP服务器对象,然后监听到文件描述符上。由于底层细节不被应用层感知,所以子进程中,开发者会有一种服务器对象就是从父进程中直接传递过来的错觉。

Node进程之间只有消息传递,不会真正的传递对象,这种错觉是抽象封装的结果。目前Node只支持我前面提到的几种句柄,并非任意类型的句柄都能在进程之间传递,除非它有完整的发送和还原的过程。

Node.js多进程架构模型

我们自己实现一个多进程架构守护Demo

深入淺析Node.js中的進程和執行緒

编写主进程

master.js 主要处理以下逻辑:

  • 创建一个 server 并监听 3000 端口。
  • 根据系统 cpus 开启多个子进程
  • 通过子进程对象的 send 方法发送消息到子进程进行通信
  • 在主进程中监听了子进程的变化,如果是自杀信号重新启动一个工作进程。
  • 主进程在监听到退出消息的时候,先退出子进程在退出主进程
// master.js
const fork = require(&#39;child_process&#39;).fork;
const cpus = require(&#39;os&#39;).cpus();

const server = require(&#39;net&#39;).createServer();
server.listen(3000);
process.title = &#39;node-master&#39;

const workers = {};
const createWorker = () => {
    const worker = fork(&#39;worker.js&#39;)
    worker.on(&#39;message&#39;, function (message) {
        if (message.act === &#39;suicide&#39;) {
            createWorker();
        }
    })
    worker.on(&#39;exit&#39;, function(code, signal) {
        console.log(&#39;worker process exited, code: %s signal: %s&#39;, code, signal);
        delete workers[worker.pid];
    });
    worker.send(&#39;server&#39;, server);
    workers[worker.pid] = worker;
    console.log(&#39;worker process created, pid: %s ppid: %s&#39;, worker.pid, process.pid);
}

for (let i=0; i<cpus.length; i++) {
    createWorker();
}

process.once(&#39;SIGINT&#39;, close.bind(this, &#39;SIGINT&#39;)); // kill(2) Ctrl-C
process.once(&#39;SIGQUIT&#39;, close.bind(this, &#39;SIGQUIT&#39;)); // kill(3) Ctrl-\
process.once(&#39;SIGTERM&#39;, close.bind(this, &#39;SIGTERM&#39;)); // kill(15) default
process.once(&#39;exit&#39;, close.bind(this));

function close (code) {
    console.log(&#39;进程退出!&#39;, code);

    if (code !== 0) {
        for (let pid in workers) {
            console.log(&#39;master process exited, kill worker pid: &#39;, pid);
            workers[pid].kill(&#39;SIGINT&#39;);
        }
    }

    process.exit(0);
}
登入後複製

工作进程

worker.js 子进程处理逻辑如下:

  • 创建一个 server 对象,注意这里最开始并没有监听 3000 端口
  • 通过 message 事件接收主进程 send 方法发送的消息
  • 监听 uncaughtException 事件,捕获未处理的异常,发送自杀信息由主进程重建进程,子进程在链接关闭之后退出
// worker.js
const http = require(&#39;http&#39;);
const server = http.createServer((req, res) => {
	res.writeHead(200, {
		&#39;Content-Type&#39;: &#39;text/plan&#39;
	});
	res.end(&#39;I am worker, pid: &#39; + process.pid + &#39;, ppid: &#39; + process.ppid);
	throw new Error(&#39;worker process exception!&#39;); // 测试异常进程退出、重启
});

let worker;
process.title = &#39;node-worker&#39;
process.on(&#39;message&#39;, function (message, sendHandle) {
	if (message === &#39;server&#39;) {
		worker = sendHandle;
		worker.on(&#39;connection&#39;, function(socket) {
			server.emit(&#39;connection&#39;, socket);
		});
	}
});

process.on(&#39;uncaughtException&#39;, function (err) {
	console.log(err);
	process.send({act: &#39;suicide&#39;});
	worker.close(function () {
		process.exit(1);
	})
})
登入後複製

Node.js 进程守护

什么是进程守护?

每次启动 Node.js 程序都需要在命令窗口输入命令 node app.js 才能启动,但如果把命令窗口关闭则Node.js 程序服务就会立刻断掉。除此之外,当我们这个 Node.js 服务意外崩溃了就不能自动重启进程了。这些现象都不是我们想要看到的,所以需要通过某些方式来守护这个开启的进程,执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。,当出现问题可以自动重启。

如何实现进程守护

这里我只说一些第三方的进程守护框架,pm2 和 forever ,它们都可以实现进程守护,底层也都是通过上面讲的 child_process 模块和 cluster 模块 实现的,这里就不再提它们的原理。

pm2 指定生产环境启动一个名为 test 的 node 服务

pm2 start app.js --env production --name test
登入後複製

pm2常用api

  • pm2 stop Name/processID 停止某个服务,通过服务名称或者服务进程ID

  • pm2 delete Name/processID 删除某个服务,通过服务名称或者服务进程ID

  • pm2 logs [Name] 查看日志,如果添加服务名称,则指定查看某个服务的日志,不加则查看所有日志

  • pm2 start app.js -i 4 集群,-i 参数用来告诉PM2以cluster_mode的形式运行你的app(对应的叫fork_mode),后面的数字表示要启动的工作线程的数量。如果给定的数字为0,PM2则会根据你CPU核心的数量来生成对应的工作线程。注意一般在生产环境使用cluster_mode模式,测试或者本地环境一般使用fork模式,方便测试到错误。

  • pm2 reload Name pm2 restart Name 应用程序代码有更新,可以用重载来加载新代码,也可以用重启来完成,reload可以做到0秒宕机加载新的代码,restart则是重新启动,生产环境中多用reload来完成代码更新!

  • pm2 show Name 查看服务详情

  • pm2 list 查看pm2中所有项目

  • pm2 monit用monit可以打开实时监视器去查看资源占用情况

pm2 官网地址:

pm2.keymetrics.io/docs/usage/…

forever 就不特殊说明了,官网地址

github.com/foreverjs/f…

注意:二者更推荐pm2,看一下二者对比就知道我为什么更推荐使用pm2了。www.jianshu.com/p/fdc12d82b…

linux 关闭一个进程

  • 查找与进程相关的PID号

    ps aux | grep server
    说明:

    root     20158  0.0  5.0 1251592 95396 ?       Sl   5月17   1:19 node /srv/mini-program-api/launch_pm2.js
登入後複製
上面是执行命令后在linux中显示的结果,第二个参数就是进程对应的PID
登入後複製
  • 杀死进程
  1. 以优雅的方式结束进程

    kill -l PID

    -l选项告诉kill命令用好像启动进程的用户已注销的方式结束进程。 当使用该选项时,kill命令也试图杀死所留下的子进程。 但这个命令也不是总能成功--或许仍然需要先手工杀死子进程,然后再杀死父进程。

  2. kill 命令用于终止进程

例如: `kill -9 [PID]`
登入後複製

-9 表示强迫进程立即停止

这个强大和危险的命令迫使进程在运行时突然终止,进程在结束后不能自我清理。
危害是导致系统资源无法正常释放,一般不推荐使用,除非其他办法都无效。
当使用此命令时,一定要通过ps -ef确认没有剩下任何僵尸进程。
只能通过终止父进程来消除僵尸进程。如果僵尸进程被init收养,问题就比较严重了。
杀死init进程意味着关闭系统。
如果系统中有僵尸进程,并且其父进程是init,
而且僵尸进程占用了大量的系统资源,那么就需要在某个时候重启机器以清除进程表了。
登入後複製
  1. killall命令

    杀死同一进程组内的所有进程。其允许指定要终止的进程的名称,而非PID。

    killall httpd

Node.js 线程

Node.js关于单线程的误区

const http = require(&#39;http&#39;);

const server = http.createServer();
server.listen(3000,()=>{
    process.title=&#39;程序员成长指北测试进程&#39;;
    console.log(&#39;进程id&#39;,process.pid)
})
登入後複製

仍然看本文第一段代码,创建了http服务,开启了一个进程,都说了Node.js是单线程,所以 Node 启动后线程数应该为 1,但是为什么会开启7个线程呢?难道Javascript不是单线程不知道小伙伴们有没有这个疑问?

解释一下这个原因:

Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。

  • 主线程:编译、执行代码。
  • 编译/优化线程:在主线程执行的时候,可以优化代码。
  • 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
  • 垃圾回收的几个线程。

所以大家常说的 Node 是单线程的指的是 JavaScript 的执行是单线程的(开发者编写的代码运行在单线程环境中),但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的因为libuv中有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的。

某些异步 IO 会占用额外的线程

还是上面那个例子,我们在定时器执行的同时,去读一个文件:

const fs = require(&#39;fs&#39;)
setInterval(() => {
    console.log(new Date().getTime())
}, 3000)

fs.readFile(&#39;./index.html&#39;, () => {})
登入後複製

线程数量变成了 11 个,这是因为在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池,而线程池默认大小为 4,因为线程数变成了 11。 我们可以手动更改线程池默认大小:

process.env.UV_THREADPOOL_SIZE = 64
登入後複製

一行代码轻松把线程变成 71。

Libuv

Libuv 是一个跨平台的异步IO库,它结合了UNIX下的libev和Windows下的IOCP的特性,最早由Node的作者开发,专门为Node提供多平台下的异步IO支持。Libuv本身是由C++语言实现的,Node中的非苏塞IO以及事件循环的底层机制都是由libuv实现的。

libuv架构图

深入淺析Node.js中的進程和執行緒

在Window环境下,libuv直接使用Windows的IOCP来实现异步IO。在非Windows环境下,libuv使用多线程来模拟异步IO。

注意下面我要说的话,Node的异步调用是由libuv来支持的,以上面的读取文件的例子,读文件实质的系统调用是由libuv来完成的,Node只是负责调用libuv的接口,等数据返回后再执行对应的回调方法。

Node.js 线程创建

直到 Node 10.5.0 的发布,官方才给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力。

先看下简单的 demo:

const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require(&#39;worker_threads&#39;);

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on(&#39;exit&#39;, code => { console.log(`main: worker stopped with exit code ${code}`); });
    worker.on(&#39;message&#39;, msg => {
      console.log(`main: receive ${msg}`);
      worker.postMessage(msg + 1);
    });
  }
}

function workerThread() {
  console.log(`worker: workerDate ${workerData}`);
  parentPort.on(&#39;message&#39;, msg => {
    console.log(`worker: receive ${msg}`);
  }),
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}
登入後複製

上述代码在主线程中开启五个子线程,并且主线程向子线程发送简单的消息。

由于 worker_thread 目前仍然处于实验阶段,所以启动时需要增加 --experimental-worker flag,运行后观察活动监视器,开启了5个子线程

深入淺析Node.js中的進程和執行緒
worker_thread 模块

worker_thread 核心代码(地址https://github.com/nodejs/node/blob/master/lib/worker_threads.js) worker_thread 模块中有 4 个对象和 2 个类,可以自己去看上面的源码。

  • isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
  • MessagePort: 用于线程之间的通信,继承自 EventEmitter。
  • MessageChannel: 用于创建异步、双向通信的通道实例。
  • threadId: 线程 ID。
  • Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
  • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
  • workerData: 用于在主进程中向子进程传递数据(data 副本)

总结

多进程 vs 多线程

对比一下多线程与多进程:

属性 多进程 多线程 比较
数据 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,同步复杂 各有千秋
CPU、内存 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 多线程更好
销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 多线程更好
coding 编码简单、调试方便 编码、调试复杂 编码、调试复杂
可靠性 进程独立运行,不会相互影响 线程同呼吸共命运 多进程更好
分布式 可用于多机多核分布式,易于扩展 只能用于多核分布式 多进程更好

更多程式相關知識,請造訪:程式設計入門! !

以上是深入淺析Node.js中的進程和執行緒的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

解釋一下explorer.exe進程是什麼 解釋一下explorer.exe進程是什麼 Feb 18, 2024 pm 12:11 PM

explorer.exe是什麼進程在我們使用Windows作業系統的時候,常常會聽到一個名詞"explorer.exe".那麼,你是否好奇這個進程到底是什麼?在本文中,我們將詳細解釋explorer.exe是什麼進程以及其功能和作用。首先,explorer.exe是Windows作業系統的關鍵流程,它負責管理和控制Windows資源管理器(Window

請問ccsvchst.exe是哪一種進程? 請問ccsvchst.exe是哪一種進程? Feb 19, 2024 pm 11:33 PM

ccsvchst.exe是一種常見的進程文件,它是SymantecEndpointProtection(SEP)軟體的一部分,而SEP則是一款由著名的網路安全公司Symantec開發的終端保護解決方案。作為該軟體的一部分,ccsvchst.exe負責管理和監控SEP的相關進程。首先,我們來了解SymantecEndpointProtection(

如何正確殺死Linux中的殭屍行程 如何正確殺死Linux中的殭屍行程 Feb 19, 2024 am 10:40 AM

在Linux系統中,殭屍行程是已經終止但仍保留在系統中的特殊流程。雖然殭屍行程不會消耗太多資源,但如果數量太多,可能會導致系統資源耗盡。本文將介紹如何正確清除殭屍進程,以確保系統的正常運作。 1Linux殭屍行程子程序完成任務後,如果父行程沒有及時檢查狀態,子行程將成為殭屍行程。子進程在等待父進程確認,完成後系統才會回收。否則,殭屍行程會繼續懸掛在系統中。若要檢查系統中是否有殭屍進程,可透過執行命令top來查看所有正在執行的進程及可能存在的殭屍進程。 ‘top’指令的結果從上圖可以看到Linux中進

Linux進程優先權調整方法詳解 Linux進程優先權調整方法詳解 Mar 15, 2024 am 08:39 AM

Linux程序優先級調整方法詳解在Linux系統中,進程的優先順序決定了其在系統中的執行順序和資源分配。合理調整進程的優先順序可以提高系統的效能和效率。本文將詳細介紹Linux中如何調整進程的優先級,並提供具體的程式碼範例。一、進程優先權概述在Linux系統中,每個行程都有一個與之相關聯的優先權。優先權的範圍一般是-20到19,其中-20表示最高優先權,19表

如何在 Windows 11 中停止工作管理員進程更新並更方便地終止任務 如何在 Windows 11 中停止工作管理員進程更新並更方便地終止任務 Aug 20, 2023 am 11:05 AM

如何在Windows11和Windows10中暫停工作管理員進程更新按CTRL+視窗鍵+刪除開啟工作管理員。預設情況下,任務管理器將開啟「進程」視窗。正如您在此處看到的,所有應用程式都在無休止地移動,當您想要選擇它們時,可能很難將它們指向下方。因此,按CTRL並按住它,這將暫停任務管理器。您仍然可以選擇應用程序,甚至可以向下捲動,但您必須始終按住CTRL按鈕。

為什麼Linux中的行程會處於休眠狀態? 為什麼Linux中的行程會處於休眠狀態? Mar 20, 2024 pm 02:09 PM

為什麼Linux中的行程會處於休眠狀態?在Linux作業系統中,進程可能會處於休眠狀態,這是由於多種不同的原因和條件造成的。當進程處於休眠狀態時,表示該進程暫時被掛起,無法繼續執行,直到滿足某種條件後才能被喚醒繼續執行。接下來將詳細介紹在Linux中進程進入休眠狀態的幾種常見情況,並透過具體的程式碼範例加以說明。等待I/O完成:當進程發起一個I/O操作(例如讀取

C++並發程式設計:如何避免執行緒飢餓和優先反轉? C++並發程式設計:如何避免執行緒飢餓和優先反轉? May 06, 2024 pm 05:27 PM

為避免執行緒飢餓,可以使用公平鎖確保資源公平分配,或設定執行緒優先權。為解決優先權反轉,可使用優先權繼承,即暫時提高持有資源執行緒的優先權;或使用鎖的提升,即提升需要資源執行緒的優先權。

C++並發程式設計:如何進行執行緒終止和取消? C++並發程式設計:如何進行執行緒終止和取消? May 06, 2024 pm 02:12 PM

C++中執行緒終止和取消機制包括:執行緒終止:std::thread::join()阻塞目前執行緒直到目標執行緒完成執行;std::thread::detach()從執行緒管理中分離目標執行緒。執行緒取消:std::thread::request_termination()請求目標執行緒終止執行;std::thread::get_id()取得目標執行緒ID,可與std::terminate()一起使用,立即終止目標執行緒。實戰中,request_termination()允許執行緒決定終止時機,join()確保在主線

See all articles