Node-js 異步 api 的本質和 libuv

Node.js 是一個 Javascript 的運行時,提供了系統能力的 api,主要是文件、網絡相關的 IO api,而 IO api 的實現是在 libuv,提供了同步異步兩種形式的 api。

本來就來探究下 libuv 的功能和提供的 api 的形式。

同步異步、事件循環

cpu 是順序執行代碼的,通過 pc 寄存器來存儲着下一條指令的內存地址。代碼的執行流程叫做控制流。但是對於一些 IO 操作來說,並不需要 cpu 做計算,而是在等待硬盤設備、網絡設備的數據讀取,這時候 cpu 是空閒的,所以一條控制流不行,會導致 cpu 利用率太低。所以操作系統又提供了進程、線程的功能,進程是分配資源的單位,而執行代碼主要是靠線程,一個線程就是一條控制流,它是 cpu 調度的基本單位,也就是說可以在多個控制流之間切換,當一個線程在做 IO 的時候就釋放 cpu,讓其他線程去跑。

一個線程阻塞的等待 IO 的方式就是同步,會比較浪費 cpu,而多個線程切換,在做 IO 的時候讓其他線程上 cpu 跑,執行完 IO 再申請 cpu 來繼續後續處理,這種方式就是異步

異步最終是多線程來實現的,但是在 Node.js 裏面又進一步通過 event loop 做了封裝,比如執行文件讀取、網絡訪問的時候並不需要開發者去創建線程,而是調用 api,指定回調函數就可以了,這是對多線程的進一步封裝。

異步的底層實現都是通過線程的切換,但是暴露給開發者的形式有兩種:

第一種是開發者控制線程的創建和銷燬,指定線程執行的內容內容,java 是這種。

第二種是提供事件循環機制,提供一系列異步 api,這些異步 api 最終是由線程來執行的,但是開發者不需要手動管理線程。javascript 是這種。

libuv

在 Node.js 裏面,實現 event loop 的就是 libuv,它是一個異步 IO 庫,負責文件和網絡的 io,提供了事件形式的異步 api。

libuv 裏有一個線程池負責維護一些線程用來執行異步 api,這個線程池的大小是可以設置的,通過環境變量 UV_THREADPOOL_SIZE 可以設置。

在 Node.js 文檔中搜索 UV_THREADPOOL_SIZE 可以看到這段介紹:

就是說 libuv 是負責 IO 的 api 的異步實現的,基於更底層的操作系統 api。這些操作系統 api 有的是異步的,有的不是,對於不是異步 api 的那些,就要由 libuv 的線程池中的線程來執行,變成異步的形式。

這些 api 包括:

這些 api 共用一個線程池,那麼肯定會相互影響,所以有的時候需要加大線程池的線程數量,默認是 4,當需要的時候可以設置這個環境變量來加大。

這讓我想起了 Node.js 的 --max-old-space-size=SIZE 可以設置堆大小一樣,都是性能調優的參數。

當面試問到 Node.js 性能調優的時候,可以答設置 libuv 的線程池大小,堆大小設置的這兩個參數 / 環境變量。

看了文檔之後,我們更確認了異步最底層的實現就是線程,只不過是通過 libuv 的線程池做了封裝。libuv 提供了 IO 相關的 api,在 Node.js 的架構中的位置如下:

IO api 的 3 種形式

梳理清楚了同步異步方式的實現原理,我們再來看下 Node.js 都怎麼提供這兩種 api 的:

同步的 api 比較簡單,就是調用函數,拿到返回值就行,很多 xxSync 的 api 都是同步的,比如:

const fs = require('fs');

const data = fs.readFileSync('tmp.txt''utf8');
console.log(data);

而異步的 api 則分爲了兩種形式,callback 和 promise:

const fs = require('fs');

fs.readFile('tmp.txt''utf8'(err, data) ={
    console.log(data);
});

還有 promise 的版本:

const fsPromises = require('fs').promises;

(async function () {
 const data = await fsPromises.readFile('tmp.txt''utf-8');
 console.log(data);
})();

其中 promise 的版本只有兩個模塊有,fs 和 dns,是在 Node.js 10.x 引入的,方便使用 async、await 來組織代碼。

const dnsPromises = require('dns').promises;

(async function () {
 const result = await dnsPromises.lookup('www.baidu.com');
 console.log(result.address);
})();

用到 fs 和 dns 這兩個模塊的時候,推薦使用 promise 形式的異步 api,當然,必須是 Node.js 10 以上的版本。

總結

程序在進行 IO 的時候, cpu 是空閒的,爲了更好的利用 cpu,操作系統提供了進程、線程的功能,一個線程就是一條控制流。當在 IO 的時候,切換到別的線程,等 IO 結束之後再繼續執行的方式就是異步,而相應的一個線程阻塞的等待的方式就是同步。

異步最終是由線程實現的,但是提供給開發者的有兩種形式:一種是提供線程 api,讓開發者自己管理線程,另一種方式就是提供事件循環,對於異步 api 通過線程來實現。第二種方式對開發者更透明,不需要關心線程的創建和切換。

Node.js 裏面的 event loop 的實現是在 libuv,它提供了文件和網絡的異步 IO 的 api,從文檔中我們可以看到,libuv 是基於操作系統的 api 實現的,而其中一些同步的 api,則是由 libuv 的線程池來執行,達到異步的目的。但是線程池的大小默認是 4,有的時候需要加大,可以通過修改 UV_THREADPOOL_SIZE 的方式。

Node.js 提供的 api 有 3 種形式,一種是同步的,一種是異步 callback、一種是異步 promise。其中異步 promises 的形式是推薦的寫法,但是隻有在 fs、dns 兩個模塊可用,並且要在 Node.js 10 以上纔行。

希望本文能夠幫大家理清異步的本質,libuv 的作用,Node.js api 的形式,以及如何做 libuv 的調優。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_GrujWmg7qA4ExxFPxij9A