nodejs非同步IO的實現
nodejs的核心之一就是非阻塞的非同步IO,於是想知道它是怎麼實現的,挖了下nodejs原始碼,找到些答案,在此跟大家分享下。首先,我用了一段js程式碼test-fs-read.js做測試,程式碼如下:
var path = require('path'), fs = require('fs'), filepath = path.join(__dirname, 'experiment.log'), fd = fs.openSync(filepath, 'r'); fs.read(fd, 12*1024*1024, 0, 'utf-8', function(err, str, bytesRead) { console.log('[main thread] execute read callback'); }); console.log('[main thread] execute operation after read');
這段程式碼的非同步IO操作就在fs.read的呼叫上,讀取的experiment.log是一個12M的文字檔案,所謂的非同步,大家大概能想得到執行時會先列印
[main thread] execute operation after read
然後列印回撥函式中的
[main thread] execute read callback
但大家也許還聽說過,nodejs是單執行緒的,那又是怎麼實現非同步IO的呢?讀檔案操作是在哪裡執行的呢?讀完又是怎麼呼叫回撥函式的呢?猜想讀文 件可能是在另一個執行緒中完成的,讀完後通過事件通知nodejs執行回撥。為了一探究竟,debug了一把nodejs和libeio原始碼,重新編譯後, 執行測試程式碼node test-fs-read.js,輸出如下:
可以看到,nodejs的IO操作是通過呼叫libeio庫完成的,debug從fs.read開始,js程式碼經過v8編譯後,fs.read會呼叫node_file.cc中的Read方法,測試程式碼的執行經歷了以下步驟:
1 node_file.cc中的Read方法呼叫libeio(eio.c)的eio_read, read請求被放入請求佇列req_queue中。
2 主執行緒建立了1個eio執行緒,此時主執行緒的read呼叫返回。
3 eio執行緒從req_queue中取出1個請求,開始執行read IO
4 主執行緒繼續執行read呼叫後的其它操作。
5 主執行緒poll eio,從響應佇列res_queue取已經完成的請求,此時res_queue為空,主執行緒stop poll
6 eio執行緒完成了read IO,read請求被放入響應佇列res_queue中,並且向主執行緒傳送libev事件want_poll(通過主執行緒初始化eio時提供的回撥函式)。
7 eio執行緒從req_queue中取下一個請求,此時已經沒有請求。
8 主執行緒響應want_poll事件,從res_queue中取出1個請求,取出請求後res_queue變為空,主執行緒傳送done_poll事件。
9 主執行緒執行請求的callback函式。
還需要說明的是,當同時有多個IO請求時,主執行緒會建立多個eio執行緒,以提高IO請求的處理速度。
為了更清晰的看到nodejs的IO執行過程,圖示如下,序號僅用來標示流程,與上述步驟序號並無對應關係。
最後總結幾條,不當之處還請大家指正。
1 nodejs通過libev事件得到IO執行狀態,而不是輪詢,提高了CPU利用率。
2 雖然nodejs是單執行緒的,但它的IO操作是多執行緒的,多個IO請求會建立多個libeio執行緒(最多4個),使通常情況的IO操作效能得到提高。
3 但是當IO操作情況比較複雜的時候,有可能造成執行緒競爭狀態,導致IO效能降低;而且libeio最多建立4個執行緒,當同時有大量IO請求時,實際效能有 待測量。另外,由於每個IO請求對應一個libeio的資料結構,當同時有大量IO操作駐留在系統中時候,會增加記憶體開銷。
4 Libeio為了實現非同步IO功能,帶來了額外的管理,當IO資料量比較小的時候,整體效能不一定比同步IO好。