为什么nodejs是单进程的_nodejs真的是单线程吗?
一、多線(xiàn)程與單線(xiàn)程
像java、python這個(gè)可以具有多線(xiàn)程的語(yǔ)言。多線(xiàn)程同步模式是這樣的,將cpu分成幾個(gè)線(xiàn)程,每個(gè)線(xiàn)程同步運(yùn)行。
而node.js采用單線(xiàn)程異步非阻塞模式,也就是說(shuō)每一個(gè)計(jì)算獨(dú)占cpu,遇到I/O請(qǐng)求不阻塞后面的計(jì)算,當(dāng)I/O完成后,以事件的方式通知,繼續(xù)執(zhí)行計(jì)算2。
事件驅(qū)動(dòng)、異步、單線(xiàn)程、非阻塞I/O,這是我們聽(tīng)得最多的關(guān)于nodejs的介紹。看到上面的關(guān)鍵字,可能我們會(huì)好奇:
為什么在瀏覽器中運(yùn)行的 Javascript 能與操作系統(tǒng)進(jìn)行如此底層的交互?
nodejs既然是單線(xiàn)程,如何實(shí)現(xiàn)異步、非阻塞I/O?
nodejs全是異步調(diào)用和非阻塞I/O,就真的不用管并發(fā)數(shù)了嗎?
nodejs事件驅(qū)動(dòng)是如何實(shí)現(xiàn)的?和瀏覽器的event loop是一回事嗎?
nodejs擅長(zhǎng)什么?不擅長(zhǎng)什么?
二、nodejs內(nèi)部揭秘
要弄清楚上面的問(wèn)題,首先要弄清楚nodejs是怎么工作的。
我們可以看到,Node.js 的結(jié)構(gòu)大致分為三個(gè)層次:
1、 Node.js 標(biāo)準(zhǔn)庫(kù),這部分是由 Javascript 編寫(xiě)的,即我們使用過(guò)程中直接能調(diào)用的 API。在源碼中的 lib 目錄下可以看到。
2、 Node bindings,這一層是 Javascript 與底層 C/C++ 能夠溝通的關(guān)鍵,前者通過(guò) bindings 調(diào)用后者,相互交換數(shù)據(jù)。
3、這一層是支撐 Node.js 運(yùn)行的關(guān)鍵,由 C/C++ 實(shí)現(xiàn)。
V8:Google 推出的 Javascript VM,也是 Node.js 為什么使用的是 Javascript 的關(guān)鍵,它為 Javascript 提供了在非瀏覽器端運(yùn)行的環(huán)境,它的高效是 Node.js 之所以高效的原因之一。
Libuv:它為 Node.js 提供了跨平臺(tái),線(xiàn)程池,事件池,異步 I/O 等能力,是 Node.js 如此強(qiáng)大的關(guān)鍵。
C-ares:提供了異步處理 DNS 相關(guān)的能力。
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數(shù)據(jù)壓縮等其他的能力。
三、libuv簡(jiǎn)介
可以看出,幾乎所有和操作系統(tǒng)打交道的部分都離不開(kāi) libuv的支持。libuv也是node實(shí)現(xiàn)跨操作系統(tǒng)的核心所在。
四、我們?cè)賮?lái)看看最開(kāi)始我拋出的問(wèn)題
問(wèn)題一:為什么在瀏覽器中運(yùn)行的 Javascript 能與操作系統(tǒng)進(jìn)行如此底層的交互?
舉個(gè)簡(jiǎn)單的例子,我們想要打開(kāi)一個(gè)文件,并進(jìn)行一些操作,可以寫(xiě)下面這樣一段代碼:
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)用過(guò)程大致可描述為:lib/fs.js → src/node_file.cc →uv_fs
從JavaScript調(diào)用Node的核心模塊,核心模塊調(diào)用C++內(nèi)建模塊,內(nèi)建模塊通過(guò) libuv進(jìn)行系統(tǒng)調(diào)用,這是Node里經(jīng)典的調(diào)用方式??傮w來(lái)說(shuō),我們?cè)?Javascript 中調(diào)用的方法,最終都會(huì)通過(guò)node-bindings 傳遞到 C/C++ 層面,最終由他們來(lái)執(zhí)行真正的操作。Node.js 即這樣與操作系統(tǒng)進(jìn)行互動(dòng)。
問(wèn)題二:nodejs既然是單線(xiàn)程,如何實(shí)現(xiàn)異步、非阻塞I/O?
順便回答標(biāo)題nodejs真的是單線(xiàn)程嗎?其實(shí)只有js執(zhí)行是單線(xiàn)程,I/O顯然是其它線(xiàn)程。
js執(zhí)行線(xiàn)程是單線(xiàn)程,把需要做的I/O交給libuv,自己馬上返回做別的事情,然后libuv在指定的時(shí)刻回調(diào)就行了。其實(shí)簡(jiǎn)化的流程就是醬紫的!細(xì)化一點(diǎn),nodejs會(huì)先從js代碼通過(guò)node-bindings調(diào)用到C/C++代碼,然后通過(guò)C/C++代碼封裝一個(gè)叫 “請(qǐng)求對(duì)象” 的東西交給libuv,這個(gè)請(qǐng)求對(duì)象里面無(wú)非就是需要執(zhí)行的功能+回調(diào)之類(lèi)的東西,給libuv執(zhí)行以及執(zhí)行完實(shí)現(xiàn)回調(diào)。
總結(jié)來(lái)說(shuō),一個(gè)異步 I/O 的大致流程如下:
1、發(fā)起 I/O 調(diào)用
用戶(hù)通過(guò) Javascript 代碼調(diào)用 Node 核心模塊,將參數(shù)和回調(diào)函數(shù)傳入到核心模塊;
Node 核心模塊會(huì)將傳入的參數(shù)和回調(diào)函數(shù)封裝成一個(gè)請(qǐng)求對(duì)象;
將這個(gè)請(qǐng)求對(duì)象推入到 I/O 線(xiàn)程池等待執(zhí)行;
Javascript 發(fā)起的異步調(diào)用結(jié)束,Javascript 線(xiàn)程繼續(xù)執(zhí)行后續(xù)操作。
2、執(zhí)行回調(diào)
I/O 操作完成后,會(huì)取出之前封裝在請(qǐng)求對(duì)象中的回調(diào)函數(shù),執(zhí)行這個(gè)回調(diào)函數(shù),以完成 Javascript 回調(diào)的目的。(這里回調(diào)的細(xì)節(jié)下面講解)
從這里,我們可以看到,我們其實(shí)對(duì) Node.js 的單線(xiàn)程一直有個(gè)誤會(huì)。事實(shí)上,它的單線(xiàn)程指的是自身 Javascript 運(yùn)行環(huán)境的單線(xiàn)程,Node.js 并沒(méi)有給 Javascript 執(zhí)行時(shí)創(chuàng)建新線(xiàn)程的能力,最終的實(shí)際操作,還是通過(guò) Libuv 以及它的事件循環(huán)來(lái)執(zhí)行的。這也就是為什么 Javascript 一個(gè)單線(xiàn)程的語(yǔ)言,能在 Node.js 里面實(shí)現(xiàn)異步操作的原因,兩者并不沖突。
問(wèn)題三:nodejs全是異步調(diào)用和非阻塞I/O,就真的不用管并發(fā)數(shù)了嗎?
之前我們就提到了線(xiàn)程池的概念,發(fā)現(xiàn)nodejs并不是單線(xiàn)程的,而且還有并行事件發(fā)生。同時(shí),線(xiàn)程池默認(rèn)大小是 4 ,也就是說(shuō),同時(shí)能有4個(gè)線(xiàn)程去做文件i/o的工作,剩下的請(qǐng)求會(huì)被掛起等待直到線(xiàn)程池有空閑。 所以nodejs對(duì)于并發(fā)數(shù),是由限制的。
線(xiàn)程池的大小可以通過(guò) UV_THREADPOOL_SIZE 這個(gè)環(huán)境變量來(lái)改變 或者在nodejs代碼中通過(guò) process.env.UV_THREADPOOL_SIZE來(lái)重新設(shè)置。
問(wèn)題四:nodejs事件驅(qū)動(dòng)是如何實(shí)現(xiàn)的?和瀏覽器的event loop是一回事嗎?
event loop是一個(gè)執(zhí)行模型,在不同的地方有不同的實(shí)現(xiàn)。瀏覽器和nodejs基于不同的技術(shù)實(shí)現(xiàn)了各自的event loop。
簡(jiǎn)單來(lái)說(shuō):
nodejs的event是基于libuv,而瀏覽器的event loop則在html5的規(guī)范中明確定義。
libuv已經(jīng)對(duì)event loop作出了實(shí)現(xiàn),而html5規(guī)范中只是定義了瀏覽器中event loop的模型,具體實(shí)現(xiàn)留給了瀏覽器廠(chǎng)商。
我們上面提到了libuv接過(guò)了js傳遞過(guò)來(lái)的 I/O請(qǐng)求,那么何時(shí)來(lái)處理回調(diào)呢?
libuv有一個(gè)事件循環(huán)(event loop)的機(jī)制,來(lái)接受和管理回調(diào)函數(shù)的執(zhí)行。
event loop是libuv的核心所在,上面我們提到 js 會(huì)把回調(diào)和任務(wù)交給libuv,libuv何時(shí)來(lái)調(diào)用回調(diào)就是 event loop 來(lái)控制的。event loop 首先會(huì)在內(nèi)部維持多個(gè)事件隊(duì)列(或者叫做觀(guān)察者 watcher),比如 時(shí)間隊(duì)列、網(wǎng)絡(luò)隊(duì)列等等,使用者可以在watcher中注冊(cè)回調(diào),當(dāng)事件發(fā)生時(shí)事件轉(zhuǎn)入pending狀態(tài),再下一次循環(huán)的時(shí)候按順序取出來(lái)執(zhí)行,而libuv會(huì)執(zhí)行一個(gè)相當(dāng)于 while true的無(wú)限循環(huán),不斷的檢查各個(gè)watcher上面是否有需要處理的pending狀態(tài)事件,如果有則按順序去觸發(fā)隊(duì)列里面保存的事件,同時(shí)由于libuv的事件循環(huán)每次只會(huì)執(zhí)行一個(gè)回調(diào),從而避免了 競(jìng)爭(zhēng)的發(fā)生。Libuv的 event loop執(zhí)行圖:
nodejs的event loop分為6個(gè)階段,每個(gè)階段的作用如下:
timers:執(zhí)行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一輪循環(huán)中有少數(shù)的I/Ocallback會(huì)被延遲到這一輪的這一階段執(zhí)行
idle, prepare:僅內(nèi)部使用
poll:最為重要的階段,執(zhí)行I/O callback,在適當(dāng)?shù)臈l件下會(huì)阻塞在這個(gè)階段
check:執(zhí)行setImmediate的callback
close callbacks:執(zhí)行close事件的callback,例如socket.on("close",func)
event loop的每一次循環(huán)都需要依次經(jīng)過(guò)上述的階段。 每個(gè)階段都有自己的callback隊(duì)列,每當(dāng)進(jìn)入某個(gè)階段,都會(huì)從所屬的隊(duì)列中取出callback來(lái)執(zhí)行,當(dāng)隊(duì)列為空或者被執(zhí)行callback的數(shù)量達(dá)到系統(tǒng)的最大數(shù)量時(shí),進(jìn)入下一階段。這六個(gè)階段都執(zhí)行完畢稱(chēng)為一輪循環(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);//更新時(shí)間變量,這個(gè)變量在uv__run_timers中會(huì)用到
uv__run_timers(loop);//timers階段
ran_pending = uv__run_pending(loop);//從libuv的文檔中可知,這個(gè)其實(shí)就是I/O callback階段,ran_pending指示隊(duì)列是否為空
uv__run_idle(loop);//idle階段
uv__run_prepare(loop);//prepare階段
timeout = 0;
/**
設(shè)置poll階段的超時(shí)時(shí)間,以下幾種情況下超時(shí)會(huì)被設(shè)為0,這意味著此時(shí)poll階段不會(huì)被阻塞,在下面的poll階段我們還會(huì)詳細(xì)討論這個(gè)
1,stop_flag不為0
2,沒(méi)有活躍的handles和request
3,idle、I/O callback、close階段的handle隊(duì)列不為空
否則,設(shè)為timer階段的callback隊(duì)列中,距離當(dāng)前時(shí)間最近的那個(gè)
**/
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ù)向前)時(shí),在所有階段結(jié)束后還會(huì)檢查一次timers,這個(gè)的邏輯的原因不太明確
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;
}
這里我們?cè)僭敿?xì)了解一下poll階段:
poll 階段有兩個(gè)主要功能:
1、執(zhí)行下限時(shí)間已經(jīng)達(dá)到的timers的回調(diào)
2、處理 poll 隊(duì)列里的事件。
當(dāng)event loop進(jìn)入 poll 階段,并且 沒(méi)有設(shè)定的timers(there are no timers scheduled),會(huì)發(fā)生下面兩件事之一:
1、如果 poll 隊(duì)列不空,event loop會(huì)遍歷隊(duì)列并同步執(zhí)行回調(diào),直到隊(duì)列清空或執(zhí)行的回調(diào)數(shù)到達(dá)系統(tǒng)上限;
2、如果 poll 隊(duì)列為空,則發(fā)生以下兩件事之一:
(1)如果代碼已經(jīng)被setImmediate()設(shè)定了回調(diào), event loop將結(jié)束 poll 階段進(jìn)入 check 階段來(lái)執(zhí)行 check 隊(duì)列(里的回調(diào))。
(2)如果代碼沒(méi)有被setImmediate()設(shè)定回調(diào),event loop將阻塞在該階段等待回調(diào)被加入 poll 隊(duì)列,并立即執(zhí)行。
但是,當(dāng)event loop進(jìn)入 poll 階段,并且 有設(shè)定的timers,一旦 poll 隊(duì)列為空(poll 階段空閑狀態(tài)):
event loop將檢查timers,如果有1個(gè)或多個(gè)timers的下限時(shí)間已經(jīng)到達(dá),event loop將繞回 timers 階段。
event loop的一個(gè)例子講述:
var fs = require('fs');
function someAsyncOperation (callback) {
// 假設(shè)這個(gè)任務(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 階段,它有個(gè)空隊(duì)列(fs.readFile()尚未結(jié)束)。所以它會(huì)等待剩下的毫秒,直到最近的timer的下限時(shí)間到了。當(dāng)它等了95ms,fs.readFile()首先結(jié)束了,然后它的回調(diào)被加到 poll的隊(duì)列并執(zhí)行——這個(gè)回調(diào)耗時(shí)10ms。之后由于沒(méi)有其它回調(diào)在隊(duì)列里,所以event loop會(huì)查看最近達(dá)到的timer的下限時(shí)間,然后回到 timers 階段,執(zhí)行timer的回調(diào)。
所以在示例里,回調(diào)被設(shè)定 和 回調(diào)執(zhí)行間的間隔是105ms。
到這里我們?cè)倏偨Y(jié)一下,整個(gè)異步IO的流程:
問(wèn)題五、nodejs擅長(zhǎng)什么?不擅長(zhǎng)什么?
Node.js 通過(guò) libuv 來(lái)處理與操作系統(tǒng)的交互,并且因此具備了異步、非阻塞、事件驅(qū)動(dòng)的能力。因此,NodeJS能響應(yīng)大量的并發(fā)請(qǐng)求。所以,NodeJS適合運(yùn)用在高并發(fā)、I/O密集、少量業(yè)務(wù)邏輯的場(chǎng)景。
上面提到,如果是 I/O 任務(wù),Node.js 就把任務(wù)交給線(xiàn)程池來(lái)異步處理,高效簡(jiǎn)單,因此 Node.js 適合處理I/O密集型任務(wù)。但不是所有的任務(wù)都是 I/O 密集型任務(wù),當(dāng)碰到CPU密集型任務(wù)時(shí),即只用CPU計(jì)算的操作,比如要對(duì)數(shù)據(jù)加解密(node.bcrypt.js),數(shù)據(jù)壓縮和解壓(node-tar),這時(shí) Node.js 就會(huì)親自處理,一個(gè)一個(gè)的計(jì)算,前面的任務(wù)沒(méi)有執(zhí)行完,后面的任務(wù)就只能干等著 。我們看如下代碼:
var start = Date.now();//獲取當(dāng)前時(shí)間戳
setTimeout(function () {
console.log(Date.now() - start);
for (var i = 0; i < 1000000000; i++){//執(zhí)行長(zhǎng)循環(huán)
}
}, 1000);
setTimeout(function () {
console.log(Date.now() - start);
}, 2000);
最終我們的打印結(jié)果是:(結(jié)果可能因?yàn)槟愕臋C(jī)器而不同)
1000
3738
對(duì)于我們期望2秒后執(zhí)行的setTimeout函數(shù)其實(shí)經(jīng)過(guò)了3738毫秒之后才執(zhí)行,換而言之,因?yàn)閳?zhí)行了一個(gè)很長(zhǎng)的for循環(huán),所以我們整個(gè)Node.js主線(xiàn)程被阻塞了,如果在我們處理100個(gè)用戶(hù)請(qǐng)求中,其中第一個(gè)有需要這樣大量的計(jì)算,那么其余99個(gè)就都會(huì)被延遲執(zhí)行。如果操作系統(tǒng)本身就是單核,那也就算了,但現(xiàn)在大部分服務(wù)器都是多 CPU 或多核的,而 Node.js 只有一個(gè) EventLoop,也就是只占用一個(gè) CPU 內(nèi)核,當(dāng) Node.js 被CPU 密集型任務(wù)占用,導(dǎo)致其他任務(wù)被阻塞時(shí),卻還有 CPU 內(nèi)核處于閑置狀態(tài),造成資源浪費(fèi)。
其實(shí)雖然Node.js可以處理數(shù)以千記的并發(fā),但是一個(gè)Node.js進(jìn)程在某一時(shí)刻其實(shí)只是在處理一個(gè)請(qǐng)求。
因此,Node.js 并不適合 CPU 密集型任務(wù)。
總結(jié)
以上是生活随笔為你收集整理的为什么nodejs是单进程的_nodejs真的是单线程吗?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python dataframe去掉索引
- 下一篇: react router 路由守卫_re