nodejs真的是單線程嗎?

[原文

1、多線程與單線程javascript

像java、python這個能夠具備多線程的語言。多線程同步模式是這樣的,將cpu分紅幾個線程,每一個線程同步運行。html

圖片描述

而node.js採用單線程異步非阻塞模式,也就是說每個計算獨佔cpu,遇到I/O請求不阻塞後面的計算,當I/O完成後,以事件的方式通知,繼續執行計算2。html5

圖片描述

事件驅動、異步、單線程、非阻塞I/O,這是咱們聽得最多的關於nodejs的介紹。看到上面的關鍵字,可能咱們會好奇:java

爲何在瀏覽器中運行的 Javascript 能與操做系統進行如此底層的交互?
nodejs既然是單線程,如何實現異步、非阻塞I/O?
nodejs全是異步調用和非阻塞I/O,就真的不用管併發數了嗎?
nodejs事件驅動是如何實現的?和瀏覽器的event loop是一回事嗎?
nodejs擅長什麼?不擅長什麼?node

2、nodejs內部揭祕

要弄清楚上面的問題,首先要弄清楚nodejs是怎麼工做的。python

圖片描述

咱們能夠看到,Node.js 的結構大體分爲三個層次:git

一、 Node.js 標準庫,這部分是由 Javascript 編寫的,即咱們使用過程當中直接能調用的 API。在源碼中的 lib 目錄下能夠看到。github

二、 Node bindings,這一層是 Javascript 與底層 C/C++ 可以溝通的關鍵,前者經過 bindings 調用後者,相互交換數據。segmentfault

三、這一層是支撐 Node.js 運行的關鍵,由 C/C++ 實現。
V8:Google 推出的 Javascript VM,也是 Node.js 爲何使用的是 Javascript 的關鍵,它爲 Javascript 提供了在非瀏覽器端運行的環境,它的高效是 Node.js 之因此高效的緣由之一。
Libuv:它爲 Node.js 提供了跨平臺,線程池,事件池,異步 I/O 等能力,是 Node.js 如此強大的關鍵。
C-ares:提供了異步處理 DNS 相關的能力。
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數據壓縮等其餘的能力。瀏覽器

3、libuv簡介

圖片描述

能夠看出,幾乎全部和操做系統打交道的部分都離不開 libuv的支持。libuv也是node實現跨操做系統的核心所在。

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

問題一:爲何在瀏覽器中運行的 Javascript 能與操做系統進行如此底層的交互?

舉個簡單的例子,咱們想要打開一個文件,並進行一些操做,能夠寫下面這樣一段代碼:

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);  };

這段代碼的調用過程大體可描述爲:lib/fs.js → src/node_file.cc →uv_fs

圖片描述

從JavaScript調用Node的核心模塊,核心模塊調用C++內建模塊,內建模塊經過 libuv進行系統調用,這是Node裏經典的調用方式。整體來講,咱們在 Javascript 中調用的方法,最終都會經過node-bindings 傳遞到 C/C++ 層面,最終由他們來執行真正的操做。Node.js 即這樣與操做系統進行互動。

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

順便回答標題nodejs真的是單線程嗎?其實只有js執行是單線程,I/O顯然是其它線程。
js執行線程是單線程,把須要作的I/O交給libuv,本身立刻返回作別的事情,而後libuv在指定的時刻回調就好了。其實簡化的流程就是醬紫的!細化一點,nodejs會先從js代碼經過node-bindings調用到C/C++代碼,而後經過C/C++代碼封裝一個叫 「請求對象」 的東西交給libuv,這個請求對象裏面無非就是須要執行的功能+回調之類的東西,給libuv執行以及執行完實現回調。

總結來講,一個異步 I/O 的大體流程以下:

一、發起 I/O 調用
用戶經過 Javascript 代碼調用 Node 核心模塊,將參數和回調函數傳入到核心模塊;
Node 核心模塊會將傳入的參數和回調函數封裝成一個請求對象;
將這個請求對象推入到 I/O 線程池等待執行;
Javascript 發起的異步調用結束,Javascript 線程繼續執行後續操做。

二、執行回調
I/O 操做完成後,會取出以前封裝在請求對象中的回調函數,執行這個回調函數,以完成 Javascript 回調的目的。(這裏回調的細節下面講解)

圖片描述

從這裏,咱們能夠看到,咱們其實對 Node.js 的單線程一直有個誤會。事實上,它的單線程指的是自身 Javascript 運行環境的單線程,Node.js 並無給 Javascript 執行時建立新線程的能力,最終的實際操做,仍是經過 Libuv 以及它的事件循環來執行的。這也就是爲何 Javascript 一個單線程的語言,能在 Node.js 裏面實現異步操做的緣由,二者並不衝突。

問題三:nodejs全是異步調用和非阻塞I/O,就真的不用管併發數了嗎?

以前咱們就提到了線程池的概念,發現nodejs並非單線程的,並且還有並行事件發生。同時,線程池默認大小是 4 ,也就是說,同時能有4個線程去作文件i/o的工做,剩下的請求會被掛起等待直到線程池有空閒。 因此nodejs對於併發數,是由限制的。
線程池的大小能夠經過 UV_THREADPOOL_SIZE 這個環境變量來改變 或者在nodejs代碼中經過 process.env.UV_THREADPOOL_SIZE來從新設置。

問題四:nodejs事件驅動是如何實現的?和瀏覽器的event loop是一回事嗎?

event loop是一個執行模型,在不一樣的地方有不一樣的實現。瀏覽器和nodejs基於不一樣的技術實現了各自的event loop。

簡單來講:

nodejs的event是基於libuv,而瀏覽器的event loop則在html5的規範中明肯定義。
libuv已經對event loop做出了實現,而html5規範中只是定義了瀏覽器中event loop的模型,具體實現留給了瀏覽器廠商。

咱們上面提到了libuv接過了js傳遞過來的 I/O請求,那麼什麼時候來處理回調呢?

libuv有一個事件循環(event loop)的機制,來接受和管理回調函數的執行。

event loop是libuv的核心所在,上面咱們提到 js 會把回調和任務交給libuv,libuv什麼時候來調用回調就是 event loop 來控制的。event loop 首先會在內部維持多個事件隊列(或者叫作觀察者 watcher),好比 時間隊列、網絡隊列等等,使用者能夠在watcher中註冊回調,當事件發生時事件轉入pending狀態,再下一次循環的時候按順序取出來執行,而libuv會執行一個至關於 while true的無限循環,不斷的檢查各個watcher上面是否有須要處理的pending狀態事件,若是有則按順序去觸發隊列裏面保存的事件,同時因爲libuv的事件循環每次只會執行一個回調,從而避免了 競爭的發生。Libuv的 event loop執行圖:

圖片描述

nodejs的event loop分爲6個階段,每一個階段的做用以下:
timers:執行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一輪循環中有少數的I/Ocallback會被延遲到這一輪的這一階段執行
idle, prepare:僅內部使用
poll:最爲重要的階段,執行I/O callback,在適當的條件下會阻塞在這個階段
check:執行setImmediate的callback
close callbacks:執行close事件的callback,例如socket.on("close",func)

event loop的每一次循環都須要依次通過上述的階段。 每一個階段都有本身的callback隊列,每當進入某個階段,都會從所屬的隊列中取出callback來執行,當隊列爲空或者被執行callback的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱爲一輪循環。

附帶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繼續的條件是如下三者之一: 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;          /** 設置poll階段的超時時間,如下幾種狀況下超時會被設爲0,這意味着此時poll階段不會被阻塞,在下面的poll階段咱們還會詳細討論這個 1,stop_flag不爲0 2,沒有活躍的handles和request 3,idle、I/O callback、close階段的handle隊列不爲空 不然,設爲timer階段的callback隊列中,距離當前時間最近的那個 **/           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(意味着流程繼續向前)時,在全部階段結束後還會檢查一次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;   }

這裏咱們再詳細瞭解一下poll階段:

poll 階段有兩個主要功能:
一、執行下限時間已經達到的timers的回調
二、處理 poll 隊列裏的事件。

當event loop進入 poll 階段,而且 沒有設定的timers(there are no timers scheduled),會發生下面兩件事之一:

一、若是 poll 隊列不空,event loop會遍歷隊列並同步執行回調,直到隊列清空或執行的回調數到達系統上限;

二、若是 poll 隊列爲空,則發生如下兩件事之一:
(1)若是代碼已經被setImmediate()設定了回調, event loop將結束 poll 階段進入 check 階段來執行 check 隊列(裏的回調)。
(2)若是代碼沒有被setImmediate()設定回調,event loop將阻塞在該階段等待回調被加入 poll 隊列,並當即執行。

可是,當event loop進入 poll 階段,而且 有設定的timers,一旦 poll 隊列爲空(poll 階段空閒狀態):
event loop將檢查timers,若是有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段。

event loop的一個例子講述:

var fs = require('fs');  function someAsyncOperation (callback) {   // 假設這個任務要消耗 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   }  });

當event loop進入 poll 階段,它有個空隊列(fs.readFile()還沒有結束)。因此它會等待剩下的毫秒,直到最近的timer的下限時間到了。當它等了95ms,fs.readFile()首先結束了,而後它的回調被加到 poll的隊列並執行——這個回調耗時10ms。以後因爲沒有其它回調在隊列裏,因此event loop會查看最近達到的timer的下限時間,而後回到 timers 階段,執行timer的回調。

因此在示例裏,回調被設定 和 回調執行間的間隔是105ms。

到這裏咱們再總結一下,整個異步IO的流程:

圖片描述

問題5、nodejs擅長什麼?不擅長什麼?

Node.js 經過 libuv 來處理與操做系統的交互,而且所以具有了異步、非阻塞、事件驅動的能力。所以,NodeJS能響應大量的併發請求。因此,NodeJS適合運用在高併發、I/O密集、少許業務邏輯的場景。

上面提到,若是是 I/O 任務,Node.js 就把任務交給線程池來異步處理,高效簡單,所以 Node.js 適合處理I/O密集型任務。但不是全部的任務都是 I/O 密集型任務,當碰到CPU密集型任務時,即只用CPU計算的操做,好比要對數據加解密(node.bcrypt.js),數據壓縮和解壓(node-tar),這時 Node.js 就會親自處理,一個一個的計算,前面的任務沒有執行完,後面的任務就只能乾等着 。咱們看以下代碼:

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

最終咱們的打印結果是:(結果可能由於你的機器而不一樣)
1000
3738

對於咱們指望2秒後執行的setTimeout函數其實通過了3738毫秒以後才執行,換而言之,由於執行了一個很長的for循環,因此咱們整個Node.js主線程被阻塞了,若是在咱們處理100個用戶請求中,其中第一個有須要這樣大量的計算,那麼其他99個就都會被延遲執行。若是操做系統自己就是單核,那也就算了,但如今大部分服務器都是多 CPU 或多核的,而 Node.js 只有一個 EventLoop,也就是隻佔用一個 CPU 內核,當 Node.js 被CPU 密集型任務佔用,致使其餘任務被阻塞時,卻還有 CPU 內核處於閒置狀態,形成資源浪費。

其實雖然Node.js能夠處理數以千記的併發,可是一個Node.js進程在某一時刻其實只是在處理一個請求。

所以,Node.js 並不適合 CPU 密集型任務。

參考文章:
https://www.cnblogs.com/chris...
https://www.cnblogs.com/onepi...
https://blog.csdn.net/scandly...
http://liyangready.github.io/...
https://blog.csdn.net/xjtrodd...
https://blog.csdn.net/sinat_2...

相關文章
相關標籤/搜索