首頁  >  文章  >  web前端  >  聊聊Node中的非同步實作與事件驅動

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

青灯夜游
青灯夜游轉載
2022-11-08 20:14:441212瀏覽

這篇文章帶大家了解一下Node中的非同步實作與事件驅動,希望對大家有幫助!

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

Node的特點

#電腦中的一些任務一般可以分割為兩個類別,一個類別叫做IO密集型,一個叫做運算密集型;對於運算密集型的任務,只能不斷榨乾CPU的效能,但是對於IO密集型的任務來說,理想情況下卻並不需要,只需要通知IO設備進行處理,過一段時間再來拿去數據就好了。 【相關教學推薦:nodejs影片教學 、程式設計影片

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

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

  • #setImmediateprocess.nextTick
  • 該小節先來解前面兩個api
它們的實作原理與非同步IO比較類似,
只是不需要IO執行緒池的參與

##setTimtoutsetInterval所建立的計時器會被插入到定時器觀察者內部的一個紅黑樹中每次tick執行的時候,會從那個紅黑樹中迭代取出定時器對象,檢查是否超過定時時間如果超過,就將這個事件(請求對象)推入到事件隊列中,在事件循環中執行其中的回調函數紅黑樹:這裡簡單提一下,就是一種特殊化的平衡二元樹,可以自平衡,查找效率基本上就是該二叉樹的深度了O(lo

######### ############n######)########################你有考慮過這個問題嗎,為什麼定時器不需要線程池的參與了呢,如果你理解了之前章節對於異步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中的非同步實作與事件驅動的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除