阻塞與非阻塞總覽

本總覽涵蓋了 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 方法都提供非同步版本,這些版本是非阻塞的,並接受回呼函式。

比較程式碼

阻塞式方法是同步執行的,而非阻塞式方法是異步執行的。

以文件系統模組為例,這是一個同步文件讀取

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read

這是一個等效的異步範例

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});

第一個範例看起來比第二個簡單,但第二行的缺點是阻塞了任何額外的 JavaScript 執行,直到整個文件被讀取完畢。請注意,在同步版本中,如果發生錯誤,必須捕獲錯誤,否則將導致進程崩潰。在異步版本中,決定是否應該拋出錯誤由作者決定。

讓我們稍微擴展一下我們的範例

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run after console.log

這裡是一個類似的,但不等效的異步範例

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork(); // will run before console.log

在上面的第一個範例中,console.log將在moreWork()之前被呼叫。在第二個範例中,fs.readFile()非阻塞式的,因此 JavaScript 執行可以繼續,moreWork()將首先被呼叫。運行moreWork()而不必等待文件讀取完成是一個關鍵的設計選擇,這允許更高的吞吐量。

並發性和吞吐量

在 Node.js 中,JavaScript 執行是單線程的,因此並發性是指事件循環在完成其他工作後執行 JavaScript 回調函數的能力。任何預期以並發方式運行的代碼都必須允許事件循環繼續運行,就像非 JavaScript 操作(如 I/O)正在發生一樣。

舉個例子,假設每個對網絡伺服器的請求需要 50 毫秒完成,其中 45 毫秒是可以異步完成的數據庫 I/O。選擇非阻塞式的異步操作可以釋放出每個請求的 45 毫秒以處理其他請求。僅僅通過選擇使用非阻塞式方法而不是阻塞式方法,這是容量上的一個顯著差異。

事件循環不同於許多其他語言中可能創建額外線程來處理並發工作的模型。

混合阻塞和非阻塞代碼的危險性

在處理輸入/輸出時應該避免一些模式。讓我們看一個例子

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
fs.unlinkSync('/file.md');

在上面的例子中,fs.unlinkSync() 可能會在 fs.readFile() 之前運行,這會導致在實際讀取之前刪除 file.md。更好的寫法是,完全 非阻塞 且保證以正確的順序執行的方法是

const fs = require('node:fs');

fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr;
  console.log(data);
  fs.unlink('/file.md', unlinkErr => {
    if (unlinkErr) throw unlinkErr;
  });
});

上面的程式碼在 fs.readFile() 的回調函數中放置了一個 非阻塞 調用 fs.unlink(),這樣保證了操作的正確順序。

其他資源