Node 並不是是真正意義上的單線程,它是主線程 "單線程",經過事件驅動模型把 I/O 和計算進行分離。javascript
它也有一個線程池(基於 C/C++ 實現的 Libuv 庫)專門負責執行那些耗時較長的 I/O 操做任務(如網絡請求、文件讀寫等),任務執行完成後會通知主線程。java
而對於 CPU 計算型任務,都是由主線程完成的。node
Node 的重要優點就是把 I/O 操做放到主線程以外,從而讓主線程騰出手去處理更多請求。web
所以 Node 擅長執行 I/O 密集型任務,不善於執行 CPU 密集型任務。瀏覽器
不知你們在接觸 Node 時是否有考慮過如下幾個問題:服務器
在解答上面的問題以前咱們先看看 NodeJS 的架構概覽圖:網絡
NodeJS 的運行機制以下圖:多線程
舉個簡單的例子,咱們想要打開一個文件,並進行一些操做,能夠寫下面這樣一段代碼:架構
var fs = require('fs');
fs.open('./test.txt', "w", function(err, fd) { //..do something});
複製代碼
這段代碼的調用過程大體可描述爲:lib/fs.js → src/node_file.cc → uv_fs併發
lib/fs.js
async function open(path, flags, mode) {
mode = modeNum(mode, 0o666);
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
return new FileHandle(
await binding.openFileHandle(pathModule.toNamespacedPath(path),stringToFlags(flags),mode, kUsePromises));
}
複製代碼
src/node_file.cc
static void Open(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
const int argc = args.Length();
if (req_wrap_async != nullptr) {
AsyncCall(env, req_wrap_async, args, "open", UTF8,AfterInteger,uv_fs_open, *path, flags, mode);
} else {
CHECK_EQ(argc, 5);
FSReqWrapSync req_wrap_sync;
FS_SYNC_TRACE_BEGIN(open);
int result = SyncCall(env, args[4], &req_wrap_sync,"open",uv_fs_open, *path, flags, mode);
FS_SYNC_TRACE_END(open);
args.GetReturnValue().Set(result);
}
}
複製代碼
uv_fs
dstfd = uv_fs_open(NULL,&fs_req,req->new_path,dst_flags,statsbuf.st_mode,NULL);
uv_fs_req_cleanup(&fs_req);
複製代碼
大體流程以下圖:
當咱們調用 fs.open
時,Node 經過 process.binding
調用 C/C++ 層面的 Open 函數,而後經過它調用 Libuv 中的具體方法 uv_fs_open
,最後執行的結果經過回調的方式傳回,完成流程。
在傳統 web 服務模型中,大多數都採用多線程來解決併發問題,由於 I/O 是阻塞的,單線程就意味着用戶要等待,顯然這是不合理的,因此建立多個線程來響應用戶請求。
Node 的單線程指的是主線程是"單線程",主線程按照編碼順序一步步執行程序代碼,若是中途遇到同步代碼阻塞,後續的程序代碼就會被卡住。
咱們能夠建立一個簡單的 Web 服務器來驗證一下:
var http = require('http');
function sleep (time) {
var _exit = Date.now() + time * 1000;
while (Date.now() < _exit) {
}
}
http.createServer(function (req, res) {
sleep(10);
res.end('server sleep 10s');
}).listen(8080);
複製代碼
經過瀏覽器快速連續請求訪問兩次 http://localhost:8080 ,可發現第一次請求在約 10s 後獲得響應,而第二次請求在約 20s 後獲得響應。
這是由於 Javascript 是解析性語言,代碼按照編碼順序一行一行被壓進 stack 裏面執行。當主線程接收到 request
請後,程序被壓進同步執行的 sleep
代碼塊(模擬業務處理)。若是在這 10s 內有第二個 request
進來就會被壓進 stack 裏面等待第一個請求執行後再處理下一個請求。以下面堆棧圖:
這也驗證了 Node 中主線程是"單線程"。
既然 Node 主線程是"單線程",那爲什麼能同時處理萬級併發而不形成阻塞呢?這就是咱們常說的 Node 基於事件驅動機制。
每一個 Node.js 進程只有一個主線程在執行程序代碼,造成一個執行棧(Execution Context Stack)。
除了主線程以外,還維護着一個 "事件隊列" (Event Queue) ,當用戶的網絡請求或其它 I/O 異步操做到來時,Node 會把它放到 Event Queue 之中,此時並不會執行它,代碼也不會阻塞,會繼續往下走,直到主線程代碼執行完畢。
主線程代碼執行完成後,經過 Event Loop(事件循環機制)開始到 Event Queue 開頭取出事件並對每一個事件從線程池中分配一個線程去執行,直到事件隊列中全部事件都執行完畢。
當有事件執行完畢後,會通知主線程執行回調方法,線程歸還回線程池。
所以 Node.js 本質上的異步操做仍是由線程池完成的,它將全部的阻塞操做都交給了內部的線程池去實現,自己只負責不斷的往返調度,從而實現異步非阻塞 I/O,這即是 Node 單線程和事件驅動的精髓之處了。
Node.js 每次事件循環都包含了 6 個階段,對應到 Libuv 源碼中的實現,以下圖所示:
setImmediate()
的回調socket
的 close
事件回調**核心函數 uv_run 源碼 **
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
//檢查loop中是否有異步任務,若是沒有直接就結束
if (!r)
uv__update_time(loop);
//事件循環其實就是一個大while
while (r != 0 && loop->stop_flag == 0) {
//更新事件階段
uv__update_time(loop);
//處理timer回調
uv__run_timers(loop);
//處理異步任務回調
ran_pending = uv__run_pending(loop);
//node內部處理階段
uv__run_idle(loop);
uv__run_prepare(loop);
// 這裏先記住 timeout 是一個時間
// uv_backend_timeout計算完畢後,傳遞給 uv__io_poll
// 若是timeout = 0,則 uv__io_poll 會直接跳過
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
//就是跑 setImmediate
uv__run_check(loop);
//關閉文件描述符等操做
uv__run_closing_handles(loop);
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;
}
複製代碼
Event loop 就是從事件隊列裏面不停循環的讀取事件,驅動了全部的異步回調函數的執行,Event Loop 總共 6 個階段,每一個階段都有一個任務隊列,當全部階段被順序執行一次後,Event Loop 完成了一個 tick。