阻塞與非阻塞概述
本概述介紹了 Node.js 中阻塞和非阻塞呼叫的區別。本概述將引用事件迴圈和 libuv,但不需要事先了解這些主題。讀者應具備 JavaScript 語言和 Node.js 回撥模式的基本知識。
“I/O”主要指與由 libuv 支援的系統磁碟和網路的互動。
阻塞
阻塞是指 Node.js 程序中額外 JavaScript 的執行必須等到一個非 JavaScript 操作完成。這是因為當阻塞操作發生時,事件迴圈無法繼續執行 JavaScript。
在 Node.js 中,由於 CPU 密集型而非等待非 JavaScript 操作(如 I/O)而表現不佳的 JavaScript,通常不被稱為阻塞。Node.js 標準庫中使用 libuv 的同步方法是最常用的阻塞操作。原生模組也可能包含阻塞方法。
Node.js 標準庫中所有的 I/O 方法都提供非同步版本,這些版本是非阻塞的,並接受回撥函式。一些方法也有阻塞的對應版本,其名稱以 Sync 結尾。
程式碼比較
阻塞方法同步執行,而非阻塞方法非同步執行。
以檔案系統模組為例,這是一個同步檔案讀取:
const = ('node:fs');
const = .('/file.md'); // blocks here until file is read
這是一個等效的非同步示例:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
});
第一個示例看起來比第二個簡單,但缺點是第二行會阻塞任何其他 JavaScript 的執行,直到整個檔案被讀取。請注意,在同步版本中,如果丟擲錯誤,需要進行捕獲,否則程序將崩潰。在非同步版本中,由作者決定是否應該丟擲錯誤,如示例所示。
讓我們稍微擴充套件一下我們的示例:
const = ('node:fs');
const = .('/file.md'); // blocks here until file is read
.();
moreWork(); // will run after console.log
這是一個相似但不等效的非同步示例:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
});
moreWork(); // will run before console.log
在上面的第一個示例中,console.log 會在 moreWork() 之前被呼叫。在第二個示例中,fs.readFile() 是非阻塞的,因此 JavaScript 的執行可以繼續,moreWork() 會先被呼叫。能夠在不等待檔案讀取完成的情況下執行 moreWork() 是一種關鍵的設計選擇,它允許更高的吞吐量。
併發和吞吐量
Node.js 中的 JavaScript 執行是單執行緒的,因此併發性指的是事件迴圈在完成其他工作後執行 JavaScript 回撥函式的能力。任何期望以併發方式執行的程式碼都必須允許事件迴圈在非 JavaScript 操作(如 I/O)發生時繼續執行。
舉個例子,假設一個 Web 伺服器的每個請求需要 50 毫秒完成,其中 45 毫秒是可以透過非同步方式完成的資料庫 I/O。選擇非阻塞的非同步操作,可以為每個請求釋放出那 45 毫秒來處理其他請求。這僅僅是透過選擇使用非阻塞方法而非阻塞方法,就在處理能力上產生了顯著的差異。
事件迴圈與許多其他語言中的模型不同,在那些模型中,可能會建立額外的執行緒來處理併發工作。
混合使用阻塞和非阻塞程式碼的危險
在處理 I/O 時,有一些模式應該避免。讓我們看一個例子:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
});
.('/file.md');
在上面的例子中,fs.unlinkSync() 很可能在 fs.readFile() 之前執行,這將在檔案實際被讀取之前刪除 file.md。一個更好的寫法是完全非阻塞且保證按正確順序執行的方式:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
.('/file.md', => {
if () {
throw ;
}
});
});
以上程式碼將一個非阻塞的 fs.unlink() 呼叫放在 fs.readFile() 的回撥函式中,這保證了操作的正確順序。