聊聊Node中的非同步實現與事件驅動

2022-11-08 22:00:34
本篇文章帶大家瞭解一下中的非同步實現與事件驅動,希望對大家有所幫助!

node.js極速入門課程:進入學習

Node的特點

計算機中的一些任務一般可以劃分為兩個類別,一個類別叫做IO密集型,一個叫做計算密集型;對於計算密集型的任務,只能不斷榨乾CPU的效能,但是對於IO密集型的任務來說,理想情況下卻並不需要,只需要通知IO裝置進行處理,過一段時間再來拿去資料就好了。【相關教學推薦: 、】

對於某些場景有一些互不相關的任務需要完成,現行的主流方法有如下兩種:

  • 多執行緒並行完成:多執行緒的代價在於建立執行緒和執行執行緒上下文切換的開銷較大。另外,在複雜的業務中,多執行緒程式設計經常面臨鎖、狀態同步等問題;
  • 單執行緒順序執行:易於表達,但序列執行的缺點在於效能,任意一個略慢的任務都會導致後續程式碼被組設

node在兩者之前給出了它的方案:利用單執行緒,遠離多執行緒死鎖、狀態同步等問題;利用非同步IO,讓單執行緒遠離阻塞,以更好地使用CPU

Node是如何實現非同步的

剛才講了node在多工處理的方案,但是node內部想要實現卻並不容易,下面介紹作業系統的幾個概念,方面後續大家更好理解,後面再講一講非同步的實現以及node的事件迴圈機制:

阻塞IO與非阻塞IO

  • 阻塞IO:應用層面發起IO呼叫之後,就一直等待資料,等作業系統核心層面完成所有操作後,呼叫才結束;

作業系統中一切皆檔案,輸入輸出裝置同樣被抽象為了檔案,核心在執行IO操作時,通過檔案描述符進行管理

  • 非阻塞IO:差別為呼叫後立即返回一個檔案描述符,並不等待,這時候CPU的時間片就可以用來處理其他事務,之後可以通過這個檔案描述符進行結果的獲取;

非阻塞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對非同步IO的實現是通過多執行緒實現的。可能會混淆的地方就是node內部雖然是多執行緒的,但是我們程式設計師開發的JavaScript程式碼卻僅僅是執行在單執行緒上的。

node通過部分執行緒進行阻塞IO或者非阻塞IO加上輪詢技術來完成資料獲取,讓一個執行緒進行計算處理,通過執行緒之間的通訊將IO得到的資料進行傳遞,這就輕鬆實現了非同步IO的模擬。

除了非同步IO,計算機中的其他資源也適用,因為linux中一切皆檔案,磁碟、硬體、通訊端等幾乎所有計算機資源都被抽象為了檔案,接下來介紹對計算機資源的呼叫都以IO為例子。

事件迴圈

在程序啟動時,node便會建立一個類似與while(true)的迴圈,每執行一次迴圈體的過程我們成為Tick

下方為node中事件迴圈流程圖:

很簡單的一張圖,簡單解釋一下:就是每次都從IO觀察者裡面獲取執行完成的事件(是個請求物件,簡單理解就是包含了請求中產生的一些資料),然後沒有回撥函數的話就繼續取出下一個事件(請求物件),有回撥就執行回撥函數

非同步IO細節

注:不同平臺有不同的細節實現,這張圖隱藏了相關平臺相容細節,比如windows下使用IOCP中的PostQueuedCompletionStatus()提交執行狀態,通過GetQueuedCompletionStatus獲取執行完成的請求,並且IOCP內部實現了執行緒池的細節,而linux等平臺通過eopll實現這個過程,並在libuv下自實現了執行緒池

setTimtoutsetInterval

除了IO等計算機資源需要非同步呼叫之外,node本身還存在一些與非同步IO無關的一些其他非同步API

  • setTimeout
  • setInterval
  • setImmediate
  • process.nextTick

該小節先講解前面兩個api

它們的實現原理與非同步IO比較類似,只是不需要IO執行緒池的參與

  • setTimtoutsetInterval建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中
  • 每次tick執行的時候,會從該紅黑樹中迭代取出定時器物件,檢查是否超過定時時間
  • 如果超過,就將這個事件(請求物件)推入到事件佇列中,在事件迴圈中執行其中的回撥函數

紅黑樹:這裡簡單提一下,就是一種特殊化的平衡二元樹,可以自平衡,查詢效率基本上就是該二元樹的深度了O(log2n)O(log_2n)

你有考慮過這個問題嗎,為什麼定時器不需要執行緒池的參與了呢,如果你理解了之前章節對於非同步IO實現原理的話,相信你應該能解釋出來,這裡簡單說說原因來加深記憶:

node中的IO執行緒池是用來呼叫IO並等待資料返回(看具體實現)的一種方式,它使JavaScript單執行緒得以非同步呼叫IO,並且不需要等待IO執行完成(因為是IO執行緒池做了),並且能獲取到最終的資料(通過觀察者模式:IO觀察者從執行緒池獲取執行完成的事件,事件迴圈機制執行後續的回撥函數)

上述這段話可能有點簡略,如果你還不明白,可以看下之前的那幾種圖~

process.nextTicksetImmediate

這兩個函數都是代表立即非同步執行一個函數,那為什麼不用setTimeout(() => { ... }, 0)來完成呢?

  • 定時器精度不夠
  • 定時器使用紅黑樹來建立定時器物件和迭代操作,浪費效能
  • process.nextTick更加輕量

輕量具體來說:我們在每次呼叫process.nextTick的時候,只會將回撥函數放入佇列中,在下一輪Tick時取出執行。定時器中採用紅黑樹的方式時O(log2n)O(log_2n)nextTickO(1)O(1)

process.nextTicksetImmediate又有什麼區別呢?畢竟它們都是將回撥函數立即非同步執行

  • 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其它相關文章!