over 1 year ago

最近碰巧討論到一個問題「NodeJS的API都是非同步的嘛?」,從這個問題延伸到「為什麼所有IO中只有fs提供同步API」,後來做了一些資料搜集,我貌似找到了答案(貌似表示 沒有自己動手研究)

要解決這個問題的第一步當然是到Github中查找Nodejs fs的原始碼,有趣的是比對Async與Sync function

fs.exists = function(path, callback) {
  if (handleError((path = getPathFromURL(path)), cb))
    return;
  if (!nullCheck(path, cb)) return;
  var req = new FSReqWrap();
  req.oncomplete = cb;
  binding.stat(pathModule._makeLong(path), req);
  function cb(err, stats) {
    if (callback) callback(err ? false : true);
  }
};
fs.existsSync = function(path) {
  try {
    handleError((path = getPathFromURL(path)));
    nullCheck(path);
    binding.stat(pathModule._makeLong(path), statValues);
    return true;
  } catch (e) {
    return false;
  }
};

結果發現,所有的Async對比Sync差別在於var req = new FSReqWrap();,這個FSReqWrap是由bindind('fs')提供,而binding是由NodeJS Core提供而非V8 Runtime!,FSReqWrap是定義於node_fs.cc的C++檔案中,因為我對於C++與Node native還不熟,所以轉往google搜尋這方面的知識,結果找到了這篇問答If nodejs uses non blocking IO, how is fs.readFileSync implemented?

Once we're in the "binding" module, we are out in v8, node_#####.cc land. The implementation of binding('fs') can be found in the node repository code, in node_file.cc.
The node engine offers overloads for the C++ calls, one taking a callback, one that does not. The node_file.cc code takes advantage of the req_wrap class. This is a wrapper for the v8 engine.
大意是:在binding模組中,我們就已經離開了V8,而是在nodejs的核心中(c++),node engine替Async提供c++的函式複寫,替V8將callback打包

this function is creating a new v8 scope object to handle the running of this event. This is where the asynchronous portion of async stuff happens. The v8 engine launches a new javascript interpreting environment to handle this particular call separately.
這個函式創建新的V8環境物件並執行事件,這也是非同步發生的地方,V8用新的JS執行環境分別執行事件。

所以要解決這個問題,就必須繼續往NodeJS底層的System call鑽研,但這部分我沒有查找到資料,如果有路過大神看到煩請指教;相反的我找到幾篇相關的文章Synchronous File IO in Node.js,這位 Dave Eddy大神用DTrace的方式研究fs.readSync()與fs.read()在呼叫system call的過程哪裡不同,結果發現沒有不同!他得到的結論是

fs.writeFileSync is synchronous in the sense that it blocks the event loop while it executes. It does NOT ask the Kernel to do a synchronous write to the underlying file system.
大意是fs.writeFileSync之所以是同步只是因為他block了event loop,他並沒有要求sytem同步寫入!

所以根本區別blocking與non-blocking在於是否block了event loop(廢話!),但是我們還是沒有解決為何只有fs提供同步API這個核心問題,後來我留意到這篇文章底下有一則留言

Filesystem operations are one of the last in Node.js to be using blocking system calls (open, preadv, pwritev) and they are normally run in worker threads to achieve non-blocking operation.
大意是檔案系統操作在NodeJS是唯一用blocking system calls!為了達到non-blocking效果他改跑在worker thread上

這跟那篇文章有點出入的地方在於 fs的底層system call是否blocking?但坦白說我比較贊成留言的說法,因為這樣才解釋得通為何fs跟其他I/O library不同之處。

最後一擊,NodeJS底層是透過libuv提供async i/o的操作,在An Introduction to libuv - Filesystem提到

The libuv filesystem operations are different from socket operation. Socket operations use the non-blocking operations provided by the operating system. Filesystem operations use blocking functions internally, but invoke these functions in a thread pool and notify watchers registered with the event loop when application interaction is required.
大意是:libuv操作檔案系統與socket不同,socket本身就是用作業系統提供non-blocking call,但是檔案系統是用內部的blocking call,分配thread執行,等到完成後通知event loop中的watcher。

DONE!

所以這幾篇文章串起來的結論是

  1. NodeJS絕大多數的I/O API都是非同步,如net、pipes、terminal等,因為他們的system call本來就是non-blocking
  2. fs是目前唯一的例外,因為他的system call是blocking,但為了提供non-blocking效果改執行在新的worker thread上,等到完成後打回event loop,這也是只有fs能提供Sync API的道理。
  • 4/18補充 19 things I learnt reading the NodeJS docs,作者翻完NodeJS官方文件後總結幾個常被忽略的Nodejs使用小撇步,其中有一章節提到 The fs module is a minefield of OS quirks,各大OS對於檔案系統操作都有各自的使用方式,Nodejs核心已經盡可能做到跨平台一致性,但可能還有潛藏Bug或是未完善的功能,像是文章中提及fs.stats()在Windows底下跟其他作業系統底下表現不一,但作者也強調不見得都是Windows vs Others的局面XD 這部分要特別留意!
← 使用Cloudflare做HTTPS部署 WebAssembly 嚐鮮 →
 
comments powered by Disqus