node.js極速入門課程:進入學習
計算機中的一些任務一般可以劃分為兩個類別,一個類別叫做IO密集型,一個叫做計算密集型;對於計算密集型的任務,只能不斷榨乾CPU的效能,但是對於IO密集型的任務來說,理想情況下卻並不需要,只需要通知IO裝置進行處理,過一段時間再來拿去資料就好了。【相關教學推薦: 、】
對於某些場景有一些互不相關的任務需要完成,現行的主流方法有如下兩種:
node
在兩者之前給出了它的方案:利用單執行緒,遠離多執行緒死鎖、狀態同步等問題;利用非同步IO,讓單執行緒遠離阻塞,以更好地使用CPU
剛才講了
node
在多工處理的方案,但是node
內部想要實現卻並不容易,下面介紹作業系統的幾個概念,方面後續大家更好理解,後面再講一講非同步的實現以及node的事件迴圈機制:
作業系統中一切皆檔案,輸入輸出裝置同樣被抽象為了檔案,核心在執行IO操作時,通過檔案描述符進行管理
非阻塞IO存在的一些問題:雖然其讓CPU的利用率提高了,但是由於立即返回的是一個檔案描述符,我們並不知道IO操作什麼時候完成,為了確認狀態變更,我們只能作輪詢操作
read
:最原始、效能最低的一種,通過重複檢查IO狀態來完成完整資料的獲取select
:通過對檔案描述符上的事件狀態來進行判斷,相對來說消耗更少;缺點就是它採用了一個1024長度的陣列來儲存狀態,所以它最多可以同時檢查1024個檔案描述符poll
:由於select
的限制,poll
改進為連結串列的儲存方式,其他的基本都一致;但是當檔案描述符較多的時候,它的效能還是非常低下的eopll
:該方案是linux
下效率最高的IO事件通知機制,在進入輪詢的時候如果沒有檢查IO事件,將會進行休眠,直到事件發生將它喚醒kqueue
:與epoll
類似,不過僅在FreeBSD系統下存在儘管epoll
利用了事件來降低對CPU的耗用,但休眠期間CPU幾乎是閒置的;我們期待的非同步IO應該是應用程式發起非阻塞呼叫,無須通過遍歷或事件喚醒等方式輪詢,可以直接處理下一個任務,只需IO完成後通過訊號或者回撥將資料傳遞給應用程式即可。
linux下還有中AIO方式就是通過訊號或回撥來傳遞資料的,不過只有Linux有,並且有限制無法利用系統快取
先說結論,node
對非同步IO的實現是通過多執行緒實現的。可能會混淆的地方就是node
內部雖然是多執行緒的,但是我們程式設計師開發的JavaScript
程式碼卻僅僅是執行在單執行緒上的。
node
通過部分執行緒進行阻塞IO或者非阻塞IO加上輪詢技術來完成資料獲取,讓一個執行緒進行計算處理,通過執行緒之間的通訊將IO得到的資料進行傳遞,這就輕鬆實現了非同步IO的模擬。
除了非同步IO,計算機中的其他資源也適用,因為linux中一切皆檔案,磁碟、硬體、通訊端等幾乎所有計算機資源都被抽象為了檔案,接下來介紹對計算機資源的呼叫都以IO為例子。
在程序啟動時,node
便會建立一個類似與while(true)
的迴圈,每執行一次迴圈體的過程我們成為Tick
;
下方為node
中事件迴圈流程圖:
很簡單的一張圖,簡單解釋一下:就是每次都從IO觀察者裡面獲取執行完成的事件(是個請求物件,簡單理解就是包含了請求中產生的一些資料),然後沒有回撥函數的話就繼續取出下一個事件(請求物件),有回撥就執行回撥函數
注:不同平臺有不同的細節實現,這張圖隱藏了相關平臺相容細節,比如windows下使用IOCP中的
PostQueuedCompletionStatus()
提交執行狀態,通過GetQueuedCompletionStatus
獲取執行完成的請求,並且IOCP內部實現了執行緒池的細節,而linux等平臺通過eopll
實現這個過程,並在libuv
下自實現了執行緒池
setTimtout
與setInterval
除了IO等計算機資源需要非同步呼叫之外,node
本身還存在一些與非同步IO無關的一些其他非同步API:
setTimeout
setInterval
setImmediate
process.nextTick
該小節先講解前面兩個api
它們的實現原理與非同步IO比較類似,只是不需要IO執行緒池的參與:
setTimtout
與setInterval
建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中tick
執行的時候,會從該紅黑樹中迭代取出定時器物件,檢查是否超過定時時間紅黑樹:這裡簡單提一下,就是一種特殊化的平衡二元樹,可以自平衡,查詢效率基本上就是該二元樹的深度了
你有考慮過這個問題嗎,為什麼定時器不需要執行緒池的參與了呢,如果你理解了之前章節對於非同步IO實現原理的話,相信你應該能解釋出來,這裡簡單說說原因來加深記憶:
node
中的IO執行緒池是用來呼叫IO並等待資料返回(看具體實現)的一種方式,它使JavaScript
單執行緒得以非同步呼叫IO,並且不需要等待IO執行完成(因為是IO執行緒池做了),並且能獲取到最終的資料(通過觀察者模式:IO觀察者從執行緒池獲取執行完成的事件,事件迴圈機制執行後續的回撥函數)
上述這段話可能有點簡略,如果你還不明白,可以看下之前的那幾種圖~
process.nextTick
與setImmediate
這兩個函數都是代表立即非同步執行一個函數,那為什麼不用setTimeout(() => { ... }, 0)
來完成呢?
process.nextTick
更加輕量輕量具體來說:我們在每次呼叫process.nextTick
的時候,只會將回撥函數放入佇列中,在下一輪Tick
時取出執行。定時器中採用紅黑樹的方式時,nextTick
為
那process.nextTick
與setImmediate
又有什麼區別呢?畢竟它們都是將回撥函數立即非同步執行
process.nextTick
的回撥執行優先順序高於setImmediate
process.nextTick
的回撥函數儲存在一個陣列中,每輪事件迴圈下全部執行,setImmediate
的結果則是儲存在連結串列中,每輪迴圈按序執行第一個回撥注意:之所以process.nextTick
的回撥執行優先順序高於setImmediate
,因為事件迴圈對觀察者的檢查是有順序的,process.nextTick
屬於idle
觀察者,setImmediate
屬於check
觀察者。iedl觀察者 > IO 觀察者 > check觀察者
對於網路通訊端的處理,
node
也應用到了非同步IO,網路通訊端上偵聽到的請求都會形成事件交給IO觀察者,事件迴圈會不停地處理這些網路IO事件,如果我們在JavaScrpt
層面上有傳入對應的回撥函數,這些回撥函數就會在事件迴圈中執行(處理這些網路請求)
常見的伺服器模型:
而node
採用的是事件驅動的方式處理這些請求,無需對每個請求建立額外的對應執行緒,可以省略掉建立執行緒和銷燬執行緒的開銷,同時作業系統的排程任務因為執行緒較少(只有node
內部實現的一些執行緒)上下文切換的代價很低。
經典問題--雪崩問題的解決:
問題描述:伺服器在剛啟動時,快取無資料,如果存取量巨大,同一條SQL
會被傳送到資料庫中反覆查詢,影響效能。
解決方案:
const proxy = new events.EventEmitter();
let status = "ready"; // 狀態鎖,避免反覆查詢
const select = function(callback) {
proxy.once("selected", callback); // 繫結一個只執行一次名為selected的事件
if(status === "ready") {
status = "pending";
// sql
db.select("SQL", (res) => {
proxy.emit("selected", res); // 觸發事件,返回查詢資料
status = "ready";
})
}
}
登入後複製
使用once
將所有請求的回撥都壓入了事件佇列中,利用其只執行一次就會將監視器移除的特點,保證每一個回撥函數只會被執行一次。對於相同的SQL語句,保證在同一個查詢開始到結束的過程中永遠只有一次。新到來的相同呼叫只需在佇列中等待資料就緒即可,一旦查詢到結果,得到的結果就可以被這些呼叫共同使用。
更多程式設計相關知識,請存取:!!
以上就是聊聊Node中的非同步實現與事件驅動的詳細內容,更多請關注TW511.COM其它相關文章!