探討 Node.js 中事件處理迴圈背後的理論
P粉564192131
P粉564192131 2024-04-01 19:35:24
0
1
463

我正在查看這個描述 JavaScript 中的文件遍歷演算法的要點

// ES6 version using asynchronous iterators, compatible with node v10.0+

const fs = require("fs");
const path = require("path");

async function* walk(dir) {
    for await (const d of await fs.promises.opendir(dir)) {
        const entry = path.join(dir, d.name);
        if (d.isDirectory()) yield* walk(entry);
        else if (d.isFile()) yield entry;
    }
}

// Then, use it with a simple async for loop
async function main() {
    for await (const p of walk('/tmp/'))
        console.log(p)
}

我被(語言特性的,而不是作者的)將 async/await 塞進演算法的每個縫隙的衝動所震驚。我不太了解 Node.js 的架構,但我認為嚴重非同步特性背後有某種意圖?對於主要是聲明性過程 C/C 線程/進程模型的我來說,這非常令人困惑和新鮮。我想知道的不是“模型是什麼”或“更好嗎”,正如人們可能有的意見,而是“驅動異步性的想法是什麼?”這是瀏覽器對響應能力的需求超過基於通用任務的效率的遺留問題嗎?

我的問題是「為什麼有這麼多非同步性?」。我不是尋求意見,而是在尋找了解 Node.js 或 JavaScript 的歷史和演變的人來解釋其架構。

重點:https://gist.github.com/lovasoa/8691344

P粉564192131
P粉564192131

全部回覆(1)
P粉328911308

嗯,在高層次上,該句子的第一部分和該句子的第二部分是衝突的。如果伺服器效率低下,那麼它將無法正確回應一堆同時到達的客戶端請求。因此,您希望提高伺服器的效率,以便它能夠盡可能回應客戶端請求。


現在,我們需要回顧幾個步驟並了解您所顯示的程式碼中到底發生了什麼。首先,雖然語言恰好是 Javascript,但程式碼的核心邏輯以及它如何使用 asyncawait 和生成器不僅僅是因為 Javascript 這種語言。這是因為Javascript運作的特定環境,在本例中是nodejs。

該環境使用事件循環並執行單一 Javascript 執行緒。其他作業系統執行緒用於各種系統事物和一些函式庫實現,但是當nodejs執行您的Javascript時,它一次只執行一段Javascript(單執行緒)。

同時,當您設計伺服器時,您希望它能夠回應大量傳入請求。您不希望它必須處理一個請求並讓所有其他請求等到第一個請求完成後才開始處理下一個請求。但是,nodejs 事件循環模型不使用多個線程,因此不會同時直接執行多個請求處理程序。

nodejs 部署的解決方案來自這樣一個事實:對於各種各樣的伺服器請求處理程序來說,主要活動以及花費大部分時間來處理請求的是I/O(例如網路、檔案I/O 或資料庫) /O)。這些是較低層級的操作,有自己的(非 Javascript)實作。

因此,它為所有 I/O 操作部署非同步模型。一個編寫良好的伺服器可以啟動非同步 I/O 操作,當它處理時(不是在 Nodejs 解釋器本身運行的程式碼中),解釋器和 NodeJS 事件循環可以自由地做其他事情,處理另一個請求。一段時間後,當該非同步操作完成時,事件將插入到事件循環中,當解釋器完成正在執行的任何操作時,它可以處理該非同步操作的結果並繼續該操作。

透過這種方式,Javascript 僅在單一執行緒中執行,但許多傳入請求可以同時「處理中」。

是的,這是一個與老式 C/C 線程模型完全不同的模型。您要么學習這種不同的模型,以便在 Nodejs 中編寫高效且有效的伺服器程式碼,要么不學習。如果您想堅持使用舊模型,那麼選擇一個不同的環境,在線程中運行請求處理程序(Java、C 等),並且旨在很好地做到這一點(當然,還有相關的設計和測試開銷)正確編寫並徹底測試所有多執行緒並發性)。

nodejs 模型的一大好處是它不容易受到多執行緒執行模型所存在的許多並發問題的影響。 Nodejs 模型也有一些缺點,偶爾需要解決方法。例如,如果您在用Javascript 編寫的請求處理程序中包含大量佔用CPU 的程式碼,那麼這仍然會使事情陷入困境,並且需要找到一種方法將佔用大量CPU 的程式碼從主事件循環中轉移到其他線程中,或進程(甚至可能是工作隊列)。但是,I/O 都是非同步的,並且可以保留在主執行緒中,而不會造成任何問題。

需要在單獨的並發線程中的程式碼越少,您可能遇到的並發錯誤就越少,並且程式碼更容易完全測試。

嗯,你想要一個循環,因為你試著循環某些東西。當您不想阻止事件循環或這是您必須完成任務的唯一操作類型(例如在資料庫中進行查找)時,您希望在該循環中使用非同步操作。

使用非同步操作並不是為了優化某些東西。這是關於編寫良好的伺服器程式碼並且不阻塞事件循環的核心設計。而且,事實上,nodejs 中的可能介面(例如資料庫介面或網路介面)僅提供非同步介面。

您提出這個問題的方式表明,您將受益於更好地理解核心 Nodejs 架構並閱讀更多有關事件循環如何工作以及異步 I/O 操作如何工作的信息。

首先,如果您使用的是非同步 API(例如網路或資料庫),那麼您別無選擇。您將進行非同步程式碼設計來使用該 API。如果您可以選擇使用非同步還是同步 API(就像在 Node.js 中使用檔案系統存取一樣),那麼您可以選擇是否在每個 API 呼叫上阻止事件循環或不阻止事件循環。如果您阻止事件循環,您將嚴重損害伺服器的可擴充性和回應能力。


該特定程式碼範例確實嘗試在與 asyncawait、生成器和 yield 相同的實作中使用非同步語言功能的廚房水槽。我一般不這樣做。這個實作的重點是能夠創建一個可以非常簡單地使用的接口,如下所示:

for await (const p of walk('/tmp/')) {
    ...
}

walk() 的內部是非同步的。此實作向 API 使用者隱藏了幾乎所有非同步實作的複雜性,這使得 API 更易於編碼。透過將單一 await 放在正確的位置,API 的使用者幾乎可以像同步一樣進行編碼。這些 Javascript 語言功能(promises、async、await、generators 等)的目的是讓非同步操作更容易編碼。

事件循環模型的優點

#程式簡單。 從執行緒存取共享資料時通常不必處理並發問題,因為所有 Javascript 都在同一執行緒中運行,因此所有共享資料存取都來自同一執行緒。您不需要互斥體來存取共享資料。您不會因這些互斥體而面臨任何死鎖的風險。

錯誤更少。 從執行緒存取公共資料要編寫無錯誤的程式碼要困難得多。如果編寫得不完美,程式碼可能會受到競爭條件的影響或缺少並發保護。而且,這些競爭條件通常很難測試,並且可能直到您的伺服器處於高負載時才會顯現出來,即使這樣也不容易重現。

更高的可擴展性(在某些情況下)。 對於主要受 I/O 限制的程式碼,協作事件循環模型可能會帶來更高的可擴展性。這是因為處理中的每個請求不會導致單獨的作業系統執行緒及其增加的開銷。相反,只有少量的應用程式狀態,通常位於與下一個回調或承諾等待相關的閉包中。

有關事件循環程式設計的文章

為什麼酷孩子使用事件循環 -這恰好是關於在 Java 程式設計中使用事件循環,但討論適用於任何環境

線程與事件的案例

#
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板