溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

nodejs是單進(jìn)程嗎

發(fā)布時間:2021-11-11 16:41:05 來源:億速云 閱讀:270 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“nodejs是單進(jìn)程嗎”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“nodejs是單進(jìn)程嗎”吧!

nodejs是單進(jìn)程。node.js采用單線程異步非阻塞模式,也就是說每一個計算獨占cpu,遇到“I/O”請求不阻塞后面的計算,當(dāng)“I/O”完成后,以事件的方式通知,繼續(xù)執(zhí)行下一個計算。

nodejs是單進(jìn)程嗎

本教程操作環(huán)境:windows7系統(tǒng)、nodejs 12.19.0版、Dell G3電腦。

一、多線程與單線程

像java、python這個可以具有多線程的語言。多線程同步模式是這樣的,將cpu分成幾個線程,每個線程同步運行。

nodejs是單進(jìn)程嗎

而node.js采用單線程異步非阻塞模式,也就是說每一個計算獨占cpu,遇到I/O請求不阻塞后面的計算,當(dāng)I/O完成后,以事件的方式通知,繼續(xù)執(zhí)行計算2。

nodejs是單進(jìn)程嗎

事件驅(qū)動、異步、單線程、非阻塞I/O,這是我們聽得最多的關(guān)于nodejs的介紹??吹缴厦娴年P(guān)鍵字,可能我們會好奇:

為什么在瀏覽器中運行的 Javascript 能與操作系統(tǒng)進(jìn)行如此底層的交互?
nodejs既然是單線程,如何實現(xiàn)異步、非阻塞I/O?
nodejs全是異步調(diào)用和非阻塞I/O,就真的不用管并發(fā)數(shù)了嗎?
nodejs事件驅(qū)動是如何實現(xiàn)的?和瀏覽器的event loop是一回事嗎?
nodejs擅長什么?不擅長什么?

二、nodejs內(nèi)部揭秘

要弄清楚上面的問題,首先要弄清楚nodejs是怎么工作的。

nodejs是單進(jìn)程嗎

我們可以看到,Node.js 的結(jié)構(gòu)大致分為三個層次:

1、    Node.js 標(biāo)準(zhǔn)庫,這部分是由 Javascript 編寫的,即我們使用過程中直接能調(diào)用的 API。在源碼中的 lib 目錄下可以看到。

2、    Node bindings,這一層是 Javascript 與底層 C/C++ 能夠溝通的關(guān)鍵,前者通過 bindings 調(diào)用后者,相互交換數(shù)據(jù)。

3、這一層是支撐 Node.js 運行的關(guān)鍵,由 C/C++ 實現(xiàn)。
V8:Google 推出的 Javascript VM,也是 Node.js 為什么使用的是 Javascript 的關(guān)鍵,它為 Javascript 提供了在非瀏覽器端運行的環(huán)境,它的高效是 Node.js 之所以高效的原因之一。
Libuv:它為 Node.js 提供了跨平臺,線程池,事件池,異步 I/O 等能力,是 Node.js 如此強(qiáng)大的關(guān)鍵。
C-ares:提供了異步處理 DNS 相關(guān)的能力。
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數(shù)據(jù)壓縮等其他的能力。

三、libuv簡介

nodejs是單進(jìn)程嗎

可以看出,幾乎所有和操作系統(tǒng)打交道的部分都離不開 libuv的支持。libuv也是node實現(xiàn)跨操作系統(tǒng)的核心所在。

四、我們再來看看最開始我拋出的問題

問題一:為什么在瀏覽器中運行的 Javascript 能與操作系統(tǒng)進(jìn)行如此底層的交互?

舉個簡單的例子,我們想要打開一個文件,并進(jìn)行一些操作,可以寫下面這樣一段代碼:

var fs = require('fs');
fs.open('./test.txt', "w", function(err, fd) {
    //..do something
});

fs.open = function(path, flags, mode, callback) { 
    // ...
    binding.open(pathModule._makeLong(path),
                        stringToFlags(flags),
 mode,
 callback); 
};

這段代碼的調(diào)用過程大致可描述為:lib/fs.js → src/node_file.cc →uv_fs

nodejs是單進(jìn)程嗎

從JavaScript調(diào)用Node的核心模塊,核心模塊調(diào)用C++內(nèi)建模塊,內(nèi)建模塊通過   libuv進(jìn)行系統(tǒng)調(diào)用,這是Node里經(jīng)典的調(diào)用方式。總體來說,我們在 Javascript 中調(diào)用的方法,最終都會通過node-bindings 傳遞到 C/C++ 層面,最終由他們來執(zhí)行真正的操作。Node.js 即這樣與操作系統(tǒng)進(jìn)行互動。

問題二:nodejs既然是單線程,如何實現(xiàn)異步、非阻塞I/O?

順便回答標(biāo)題nodejs真的是單線程嗎?其實只有js執(zhí)行是單線程,I/O顯然是其它線程。
js執(zhí)行線程是單線程,把需要做的I/O交給libuv,自己馬上返回做別的事情,然后libuv在指定的時刻回調(diào)就行了。其實簡化的流程就是醬紫的!細(xì)化一點,nodejs會先從js代碼通過node-bindings調(diào)用到C/C++代碼,然后通過C/C++代碼封裝一個叫 “請求對象” 的東西交給libuv,這個請求對象里面無非就是需要執(zhí)行的功能+回調(diào)之類的東西,給libuv執(zhí)行以及執(zhí)行完實現(xiàn)回調(diào)。

總結(jié)來說,一個異步 I/O 的大致流程如下:

1、發(fā)起 I/O 調(diào)用
用戶通過 Javascript 代碼調(diào)用 Node 核心模塊,將參數(shù)和回調(diào)函數(shù)傳入到核心模塊;
Node 核心模塊會將傳入的參數(shù)和回調(diào)函數(shù)封裝成一個請求對象;
將這個請求對象推入到 I/O 線程池等待執(zhí)行;
Javascript 發(fā)起的異步調(diào)用結(jié)束,Javascript 線程繼續(xù)執(zhí)行后續(xù)操作。

2、執(zhí)行回調(diào)
I/O 操作完成后,會取出之前封裝在請求對象中的回調(diào)函數(shù),執(zhí)行這個回調(diào)函數(shù),以完成 Javascript 回調(diào)的目的。(這里回調(diào)的細(xì)節(jié)下面講解)

nodejs是單進(jìn)程嗎

從這里,我們可以看到,我們其實對 Node.js 的單線程一直有個誤會。事實上,它的單線程指的是自身 Javascript 運行環(huán)境的單線程,Node.js 并沒有給 Javascript 執(zhí)行時創(chuàng)建新線程的能力,最終的實際操作,還是通過 Libuv 以及它的事件循環(huán)來執(zhí)行的。這也就是為什么 Javascript 一個單線程的語言,能在 Node.js 里面實現(xiàn)異步操作的原因,兩者并不沖突。

問題三:nodejs全是異步調(diào)用和非阻塞I/O,就真的不用管并發(fā)數(shù)了嗎?

之前我們就提到了線程池的概念,發(fā)現(xiàn)nodejs并不是單線程的,而且還有并行事件發(fā)生。同時,線程池默認(rèn)大小是 4 ,也就是說,同時能有4個線程去做文件i/o的工作,剩下的請求會被掛起等待直到線程池有空閑。 所以nodejs對于并發(fā)數(shù),是由限制的。
線程池的大小可以通過 UV_THREADPOOL_SIZE 這個環(huán)境變量來改變 或者在nodejs代碼中通過 process.env.UV_THREADPOOL_SIZE來重新設(shè)置。

問題四:nodejs事件驅(qū)動是如何實現(xiàn)的?和瀏覽器的event loop是一回事嗎?

event loop是一個執(zhí)行模型,在不同的地方有不同的實現(xiàn)。瀏覽器和nodejs基于不同的技術(shù)實現(xiàn)了各自的event loop。

簡單來說:

nodejs的event是基于libuv,而瀏覽器的event loop則在html5的規(guī)范中明確定義。
libuv已經(jīng)對event loop作出了實現(xiàn),而html5規(guī)范中只是定義了瀏覽器中event loop的模型,具體實現(xiàn)留給了瀏覽器廠商。

我們上面提到了libuv接過了js傳遞過來的 I/O請求,那么何時來處理回調(diào)呢?

libuv有一個事件循環(huán)(event loop)的機(jī)制,來接受和管理回調(diào)函數(shù)的執(zhí)行。

event loop是libuv的核心所在,上面我們提到 js 會把回調(diào)和任務(wù)交給libuv,libuv何時來調(diào)用回調(diào)就是 event loop 來控制的。event loop 首先會在內(nèi)部維持多個事件隊列(或者叫做觀察者 watcher),比如 時間隊列、網(wǎng)絡(luò)隊列等等,使用者可以在watcher中注冊回調(diào),當(dāng)事件發(fā)生時事件轉(zhuǎn)入pending狀態(tài),再下一次循環(huán)的時候按順序取出來執(zhí)行,而libuv會執(zhí)行一個相當(dāng)于 while true的無限循環(huán),不斷的檢查各個watcher上面是否有需要處理的pending狀態(tài)事件,如果有則按順序去觸發(fā)隊列里面保存的事件,同時由于libuv的事件循環(huán)每次只會執(zhí)行一個回調(diào),從而避免了 競爭的發(fā)生。Libuv的 event loop執(zhí)行圖:

nodejs是單進(jìn)程嗎

nodejs的event loop分為6個階段,每個階段的作用如下:
timers:執(zhí)行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一輪循環(huán)中有少數(shù)的I/Ocallback會被延遲到這一輪的這一階段執(zhí)行
idle, prepare:僅內(nèi)部使用
poll:最為重要的階段,執(zhí)行I/O callback,在適當(dāng)?shù)臈l件下會阻塞在這個階段
check:執(zhí)行setImmediate的callback
close callbacks:執(zhí)行close事件的callback,例如socket.on("close",func)

event loop的每一次循環(huán)都需要依次經(jīng)過上述的階段。  每個階段都有自己的callback隊列,每當(dāng)進(jìn)入某個階段,都會從所屬的隊列中取出callback來執(zhí)行,當(dāng)隊列為空或者被執(zhí)行callback的數(shù)量達(dá)到系統(tǒng)的最大數(shù)量時,進(jìn)入下一階段。這六個階段都執(zhí)行完畢稱為一輪循環(huán)。

附帶event loop 源碼:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    int timeout;
    int r;
    int ran_pending;
  
    /*
    從uv__loop_alive中我們知道event loop繼續(xù)的條件是以下三者之一:
    1,有活躍的handles(libuv定義handle就是一些long-lived objects,例如tcp server這樣)
    2,有活躍的request
    3,loop中的closing_handles
    */
    r = uv__loop_alive(loop);
    if (!r)
      uv__update_time(loop);
  
    while (r != 0 && loop->stop_flag == 0) {
      uv__update_time(loop);//更新時間變量,這個變量在uv__run_timers中會用到
      uv__run_timers(loop);//timers階段
      ran_pending = uv__run_pending(loop);//從libuv的文檔中可知,這個其實就是I/O callback階段,ran_pending指示隊列是否為空
      uv__run_idle(loop);//idle階段
      uv__run_prepare(loop);//prepare階段
  
      timeout = 0;
  
      /**
      設(shè)置poll階段的超時時間,以下幾種情況下超時會被設(shè)為0,這意味著此時poll階段不會被阻塞,在下面的poll階段我們還會詳細(xì)討論這個
      1,stop_flag不為0
      2,沒有活躍的handles和request
      3,idle、I/O callback、close階段的handle隊列不為空
      否則,設(shè)為timer階段的callback隊列中,距離當(dāng)前時間最近的那個
      **/    
      if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
        timeout = uv_backend_timeout(loop);
  
      uv__io_poll(loop, timeout);//poll階段
      uv__run_check(loop);//check階段
      uv__run_closing_handles(loop);//close階段
      //如果mode == UV_RUN_ONCE(意味著流程繼續(xù)向前)時,在所有階段結(jié)束后還會檢查一次timers,這個的邏輯的原因不太明確
      
      if (mode == UV_RUN_ONCE) {
        uv__update_time(loop);
        uv__run_timers(loop);
      }
  
      r = uv__loop_alive(loop);
      if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
        break;
    }
  
    if (loop->stop_flag != 0)
      loop->stop_flag = 0;
  
    return r;
  }

這里我們再詳細(xì)了解一下poll階段:

poll 階段有兩個主要功能:
1、執(zhí)行下限時間已經(jīng)達(dá)到的timers的回調(diào)
2、處理 poll 隊列里的事件。

當(dāng)event loop進(jìn)入 poll 階段,并且 沒有設(shè)定的timers(there are no timers scheduled),會發(fā)生下面兩件事之一:

1、如果 poll 隊列不空,event loop會遍歷隊列并同步執(zhí)行回調(diào),直到隊列清空或執(zhí)行的回調(diào)數(shù)到達(dá)系統(tǒng)上限;

2、如果 poll 隊列為空,則發(fā)生以下兩件事之一:
(1)如果代碼已經(jīng)被setImmediate()設(shè)定了回調(diào), event loop將結(jié)束 poll 階段進(jìn)入 check 階段來執(zhí)行 check 隊列(里的回調(diào))。
(2)如果代碼沒有被setImmediate()設(shè)定回調(diào),event loop將阻塞在該階段等待回調(diào)被加入 poll 隊列,并立即執(zhí)行。

但是,當(dāng)event loop進(jìn)入 poll 階段,并且 有設(shè)定的timers,一旦 poll 隊列為空(poll 階段空閑狀態(tài)):
event loop將檢查timers,如果有1個或多個timers的下限時間已經(jīng)到達(dá),event loop將繞回 timers 階段。

event loop的一個例子講述:

var fs = require('fs');

function someAsyncOperation (callback) {
  // 假設(shè)這個任務(wù)要消耗 95ms
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);

// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {

  var startCallback = Date.now();

  // 消耗 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});

當(dāng)event loop進(jìn)入 poll 階段,它有個空隊列(fs.readFile()尚未結(jié)束)。所以它會等待剩下的毫秒,直到最近的timer的下限時間到了。當(dāng)它等了95ms,fs.readFile()首先結(jié)束了,然后它的回調(diào)被加到 poll的隊列并執(zhí)行——這個回調(diào)耗時10ms。之后由于沒有其它回調(diào)在隊列里,所以event loop會查看最近達(dá)到的timer的下限時間,然后回到 timers 階段,執(zhí)行timer的回調(diào)。

所以在示例里,回調(diào)被設(shè)定 和 回調(diào)執(zhí)行間的間隔是105ms。

到這里我們再總結(jié)一下,整個異步IO的流程:

nodejs是單進(jìn)程嗎

問題五、nodejs擅長什么?不擅長什么?

Node.js 通過 libuv 來處理與操作系統(tǒng)的交互,并且因此具備了異步、非阻塞、事件驅(qū)動的能力。因此,NodeJS能響應(yīng)大量的并發(fā)請求。所以,NodeJS適合運用在高并發(fā)、I/O密集、少量業(yè)務(wù)邏輯的場景。

上面提到,如果是 I/O 任務(wù),Node.js 就把任務(wù)交給線程池來異步處理,高效簡單,因此 Node.js 適合處理I/O密集型任務(wù)。但不是所有的任務(wù)都是 I/O 密集型任務(wù),當(dāng)碰到CPU密集型任務(wù)時,即只用CPU計算的操作,比如要對數(shù)據(jù)加解密(node.bcrypt.js),數(shù)據(jù)壓縮和解壓(node-tar),這時 Node.js 就會親自處理,一個一個的計算,前面的任務(wù)沒有執(zhí)行完,后面的任務(wù)就只能干等著 。我們看如下代碼:

var start = Date.now();//獲取當(dāng)前時間戳
setTimeout(function () {
    console.log(Date.now() - start);
    for (var i = 0; i < 1000000000; i++){//執(zhí)行長循環(huán)
    }
}, 1000);
setTimeout(function () {
    console.log(Date.now() - start);
}, 2000);

最終我們的打印結(jié)果是:(結(jié)果可能因為你的機(jī)器而不同)
1000
3738

對于我們期望2秒后執(zhí)行的setTimeout函數(shù)其實經(jīng)過了3738毫秒之后才執(zhí)行,換而言之,因為執(zhí)行了一個很長的for循環(huán),所以我們整個Node.js主線程被阻塞了,如果在我們處理100個用戶請求中,其中第一個有需要這樣大量的計算,那么其余99個就都會被延遲執(zhí)行。如果操作系統(tǒng)本身就是單核,那也就算了,但現(xiàn)在大部分服務(wù)器都是多 CPU 或多核的,而 Node.js 只有一個 EventLoop,也就是只占用一個 CPU 內(nèi)核,當(dāng) Node.js 被CPU 密集型任務(wù)占用,導(dǎo)致其他任務(wù)被阻塞時,卻還有 CPU 內(nèi)核處于閑置狀態(tài),造成資源浪費。

其實雖然Node.js可以處理數(shù)以千記的并發(fā),但是一個Node.js進(jìn)程在某一時刻其實只是在處理一個請求。

因此,Node.js 并不適合 CPU 密集型任務(wù)。

感謝各位的閱讀,以上就是“nodejs是單進(jìn)程嗎”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對nodejs是單進(jìn)程嗎這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI