nodejs是單進程。Node遵循的是單線程單進程的模式,但其基于事件驅動、異步非阻塞模式,可以應用于高并發場景,避免了線程創建、線程之間上下文切換所產生的資源開銷。
本教程操作環境:windows7系統、nodejs 12.19.0版、Dell G3電腦。
進程
進程 Process 是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎,進程是線程的容器 (來自百科) 。進程是資源分配的最小單位。我們啟動一個服務、運行一個實例,就是開一個服務進程,例如 Java 里的 JVM 本身就是一個進程,Node.js 里通過 node app.js 開啟一個服務進程,多進程就是進程的復制 (fork) ,fork 出來的每個進程都擁有自己的獨立空間地址、數據棧,一個進程無法訪問另外一個進程里定義的變量、數據結構,只有建立了 IPC 通信,進程之間才可數據共享。
線程
線程是操作系統能夠進行運算調度的最小單位,首先我們要清楚線程是隸屬于進程的,被包含于進程之中。 一個線程只能隸屬于一個進程,但是一個進程是可以擁有多個線程的。 單線程 單線程就是一個進程只開一個線程 Javascript 就是屬于單線程,程序順序執行(這里暫且不提JS異步),可以想象一下隊列,前面一個執行完之后,后面才可以執行,當你在使用單線程語言編碼時切勿有過多耗時的同步操作,否則線程會造成阻塞,導致后續響應無法處理。你如果采用 Javascript 進行編碼時候,請盡可能的利用 Javascript 異步操作的特性。
nodejs單進程單線程事件驅動
Node遵循的是單線程單進程的模式,node的單線程是指js的引擎只有一個實例,且在nodejs的主線程中執行,同時node以事件驅動的方式處理IO等異步操作。node的單線程模式,只維持一個主線程,大大減少了線程間切換的開銷,但是會有多個worker線程,用于執行異步操作。
但是node的單線程使得在主線程不能進行CPU密集型操作,否則會阻塞主線程。對于CPU密集型操作,在node中通過 child_process 可以創建獨立的子進程,父子進程通過IPC通信,子進程可以是外部應用也可以是node子程序,子進程執行后可以將結果返回給父進程。
Node.js的運行機制
- V8引擎解析JavaScript腳本。
- 解析后的代碼,調用Node API。
- libuv庫負責Node API的執行。它將不同的任務分配給不同的worker線程,形成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。
- V8引擎再將結果返回給用戶。
這個圖是整個 Node.js 的運行原理,從左到右,從上到下,Node.js 被分為了四層,分別是 應用層、V8引擎層、Node API層 和 LIBUV層。
- 應用層: 即 JavaScript 交互層,常見的就是 Node.js 的模塊,比如 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互
- NodeAPI層: 為上層模塊提供系統調用,一般是由 C 語言來實現,和操作系統進行交互 。
- LIBUV層: 是跨平臺的底層封裝,實現了 事件循環、文件操作等,是 Node.js 實現異步的核心
Node.js 事件循環
Node.js 通常情況下是單進程的。
- 主線程運行 V8 和 Javascript
- 多個子線程通過 事件循環 被調度
事件循環:
事件循環是一種編程構造,用于等待和分派程序中的事件或消息, 主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環)
事件隊列:
當用戶的網絡請求或者其它的異步操作到來時,node都會把它放到Event Queue之中,此時并不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
任務隊列:
任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列",就是讀取里面有哪些事件。
事件驅動:
實質是通過主循環加事件觸發方式運行程序
node
Node.js 不是一門語言也不是框架,它只是基于 Google V8 引擎的 JavaScript 運行時環境,是對 js 功能的拓展。提供了網絡、文件、dns 解析、進程線程等功能。
libuv
libuv是專門為Node.js開發的一個封裝庫,提供跨平臺的異步I/O能力。
注意:
-
一個事件循環有一個或多個任務隊列。一個任務隊列是一組的任務
-
Libuv 主要是,利用系統提供的事件驅動模塊解決網絡異步 IO,利用線程池解決文件IO。另外還實現了定時器,對進程、線程等使用進行了封裝。
其實這里的事件循環和js在瀏覽器的事件循環是一樣的,主線程允許同步代碼,異步代碼放到對應的工作線程中執行,回調執行結果后放進事件隊列,待主線程執行事件隊列的任務。
事件驅動+事件循環實現高并發
具體執行順序:
1、每個Node.js進程只有一個主線程在執行程序代碼,形成一個執行棧(execution context stack)
2、主線程之外,還維護了一個"事件隊列"(Event queue)。當用戶的網絡請求或者其它的異步操作到來時,node都會把它放到Event Queue之中,此時并不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
3、主線程代碼執行完畢完成后,然后通過Event Loop,也就是事件循環機制,開始到Event Queue的開頭取出第一個事件,從線程池中分配一個線程去執行這個事件,接下來繼續取出第二個事件,再從線程池中分配一個線程去執行,然后第三個,第四個。主線程不斷的檢查事件隊列中是否有未執行的事件,直到事件隊列中所有事件都執行完了,此后每當有新的事件加入到事件隊列中,都會通知主線程按順序取出交EventLoop處理。當有事件執行完畢后,會通知主線程,主線程執行回調,線程歸還給線程池。
注意
我們所看到的node.js單線程只是一個js主線程與ui渲染共享一個線程,本質上的異步操作還是由線程池完成的,node將所有的阻塞操作都交給了內部的線程池去實現,本身只負責不斷的往返調度,并沒有進行真正的I/O操作,從而實現異步非阻塞I/O,這便是node單線程和事件驅動的精髓之處了。
總結:
1、libuv 線程池默認打開 4 個,最多打開 128個 線程。(例如:以前 web 服務器同一時間比如說最多只能接收 100 個請求,多的就無法接收了,服務器就掛掉了。nodejs 所謂的高并發是指可以同時接收 1000、10000 個請求,只不過以排隊的方式在等待。)
2、主線程執行js,是單線程的,js代碼在做大量計算就是cpu密集了。主線程不空閑出來也沒法處理 io 的事,所以就會阻塞了。
3、回調只能保證某個請求按照順序執行,不能保證多個請求訪問一個資源的先后順序,多個請求訪問一個資源是要加鎖的,用事務加鎖就行。
【推薦學習:《nodejs 教程》】