天天看點

Node.js 阻塞與非阻塞概述

作者:提拉米蘇TiLaMiSu

本概述介紹了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 fs = require('fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read           

一個異步示例:

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

第一個例子看起來比第二個簡單,但缺點是第二行會阻止任何附加JavaScript的執行,直到整個檔案被讀取。請注意,在同步版本中,如果抛出錯誤,則需要捕獲錯誤,否則程序将崩潰。在異步版本中,由作者決定是否應該抛出錯誤。

我們稍微擴充一下我們的示例:

const fs = require('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('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)發生時繼續運作。

作為一個例子,讓我們考慮這樣一種情況,即對web伺服器的每個請求需要50毫秒才能完成,其中45毫秒是可以異步完成的資料庫I/O。選擇非阻塞異步操作可以為每個請求釋放45ms的時間來處理其他請求。僅僅通過選擇使用非阻塞方法而不是阻塞方法,這在容量上是一個顯著的差異。

事件循環不同于許多其他語言中的模型,在這些語言中可以建立額外的線程來處理并發工作。

混合阻塞和非阻塞代碼

在處理I/O時,應該避免一些方式。讓我們看一個例子:

const fs = require('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('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()的非阻塞調用,這保證了操作的正确順序。