淺析 Node.js 單執行緒模型
總結筆記:對於每個使用者請求,由主執行緒接收並存放於一個事件佇列中(不做任何處理),當無請求發生時,即主執行緒空閒,主執行緒開始迴圈處理事件佇列中的任務:
對於非阻塞JS程式:
1、若某事件需要I/O操作,則主執行緒發出I/O請求,然後繼續執行,由底層的程式實現I/O並返回I/O資料(底層程式是多執行緒的,JS是單執行緒的),底層I/O執行緒處理完後將該事件重新放入事件佇列並釋放當前執行緒;
2、某事件不需要I/O操作,則主執行緒直接處理;(由其他執行緒處理後放入的事件此時也被主執行緒直接處理掉);
對於阻塞JS程式:
1、若某事件需要I/O操作,則主執行緒發出I/O請求,然後等待I/O結束,由底層的程式實現I/O並返回I/O資料,主執行緒獲得該事件所需資料後繼續處理該事件;
2、某事件不需要I/O操作,則主執行緒直接處理;
綜上可知,node.js由js解釋程式和底層程式碼實現,JS程式碼是主執行緒,是單執行緒執行,而底層程式碼是多執行緒,可同時處理多個I/O請求,js中的阻塞與非阻塞程式碼只決定js在I/O時繼不繼續執行(當然,若阻塞執行,底層多執行緒也沒啥用了),而底層會為每一個I/O請求建立一個執行緒;
注意:這只是對Node.js的一個分析,用來理解nodejs的執行緒模型而已,實際使用要具體問題具體分析,建議結合http://www.runoob.com/nodejs/nodejs-callback.html中的阻塞與非阻塞來學習,阻塞即只要一個主執行緒執行所有操作,當事件需要I/O操作則主執行緒等待I/O完成再繼續執行,而非阻塞,即對事件處理使用了事件回撥,此時,主執行緒將繼續執行下一步的程式碼而不用等待該事件I/O完成,當I/O完成時主執行緒再針對該事件執行相應的回撥函式;
例如:1、
var http =require('http'); http.createServer(function(request, response){// 傳送 HTTP 頭部 // HTTP 狀態值: 200 : OK// 內容型別: text/plain response.writeHead(200,{'Content-Type':'text/plain'});// 傳送響應資料 "Hello World" response.end('Hello World\n');}).listen(8888);// 終端列印如下資訊 console.log('Server running at http://127.0.0.1:8888/'該主執行緒只做三件事:1、偵聽8888埠(偵聽操作也可以理解為是I/O操作,因而應當也是由底層程式實現,即底層程式監聽埠,若有事件,則放入事件佇列,繼續偵聽埠); 2、JS主執行緒處理並生成返回資料; 3、返回處理結果(此步驟是I/O操作,由執行緒池處理););
2、
var fs =require("fs"); fs.readFile('input.txt',function(err, data){if(err)return console.error(err); console.log(data.toString());}); console.log("程式執行結束!");該主執行緒在執行I/O時不等待I/O完成,直接繼續執行,執行緒池執行緒執行完後將結果返還給主執行緒,主執行緒執行回撥函式並處理事件;
正文
Node.js 採用事件驅動和非同步 I/O 的方式,實現了一個單執行緒、高併發的 JavaScript 執行時環境,而單執行緒就意味著同一時間只能做一件事,那麼 Node.js 如何通過單執行緒來實現高併發和非同步 I/O?本文將圍繞這個問題來探討 Node.js 的單執行緒模型 。
1、高併發策略
一般來說,高併發的解決方案就是提供多執行緒模型,伺服器為每個客戶端請求分配一個執行緒,使用同步 I/O,系統通過執行緒切換來彌補同步 I/O 呼叫的時間開銷。比如 Apache 就是這種策略,由於 I/O 一般都是耗時操作,因此這種策略很難實現高效能,但非常簡單,可以實現複雜的互動邏輯。
而事實上,大多數網站的伺服器端都不會做太多的計算,它們接收到請求以後,把請求交給其它服務來處理(比如讀取資料庫),然後等著結果返回,最後再把結果發給客戶端。因此,Node.js 針對這一事實採用了單執行緒模型來處理,它不會為每個接入請求分配一個執行緒,而是用一個主執行緒處理所有的請求,然後對 I/O 操作進行非同步處理,避開了建立、銷燬執行緒以及線上程間切換所需的開銷和複雜性。
2、事件迴圈
Node.js 在主執行緒裡維護了一個事件佇列,當接到請求後,就將該請求作為一個事件放入這個佇列中,然後繼續接收其他請求。當主執行緒空閒時(沒有請求接入時),就開始迴圈事件佇列,檢查佇列中是否有要處理的事件,這時要分兩種情況:如果是非 I/O 任務,就親自處理,並通過回撥函式返回到上層呼叫;如果是 I/O 任務,就從 執行緒池 中拿出一個執行緒來處理這個事件,並指定回撥函式,然後繼續迴圈佇列中的其他事件。
當執行緒中的 I/O 任務完成以後,就執行指定的回撥函式,並把這個完成的事件放到事件佇列的尾部,等待事件迴圈,當主執行緒再次迴圈到該事件時,就直接處理並返回給上層呼叫。 這個過程就叫 事件迴圈 (Event Loop),其執行原理如下圖所示:
這個圖是整個 Node.js 的執行原理,從左到右,從上到下,Node.js 被分為了四層,分別是 應用層、V8引擎層、Node API層 和 LIBUV層。
- 應用層: 即 JavaScript 互動層,常見的就是 Node.js 的模組,比如 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 互動
- NodeAPI層: 為上層模組提供系統呼叫,一般是由 C 語言來實現,和作業系統進行互動 。
- LIBUV層: 是跨平臺的底層封裝,實現了 事件迴圈、檔案操作等,是 Node.js 實現非同步的核心 。
無論是 Linux 平臺還是 Windows 平臺,Node.js 內部都是通過 執行緒池 來完成非同步 I/O 操作的,而 LIBUV 針對不同平臺的差異性實現了統一呼叫。因此,Node.js 的單執行緒僅僅是指 JavaScript 執行在單執行緒中,而並非 Node.js 是單執行緒。
3、事件驅動模型
Node.js 實現非同步的核心是事件驅動,也就是說,它把每一個任務都當成 事件 來處理,然後通過 Event Loop 模擬了非同步的效果,為了更具體、更清晰的理解和接受這個事實,下面我們用虛擬碼來描述一下這個實現過程 。
【1】定義事件佇列
既然是佇列,那就是一個先進先出 (FIFO) 的資料結構,我們用JS陣列來描述,如下:
1 2 3 4 5 6 7 |
/**
*
定義事件佇列
*
入隊:push()
*
出隊:shift()
*
空佇列:length == 0
*/
globalEventQueue:
[]
|
我們利用陣列來模擬佇列結構:陣列的第一個元素是佇列的頭部,陣列的最後一個元素是佇列的尾部,push() 就是在佇列尾部插入一個元素,shift() 就是從佇列頭部彈出一個元素。這樣就實現了一個簡單的事件佇列。
【2】定義接收請求入口
每一個請求都會被攔截並進入處理函式,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/**
|