1 nodejs 中的異步存在嗎?
現在有點 javascript 基礎的人都在聽說過 nodejs ,而只要與 javascript 打交到人都會用或者是將要使用 nodejs 。畢竟 nodejs 的生態很強大,與 javascript 相關的工具也做的很方便,很好用。
javascript 語言很小巧,但是一旦與 nodejs 中的運行環境放在一起,有些概念就很難理解,特別是異步
的概念。有人會說不會啊,很好理解啊?不就是一個ajax
請求加上一個回調函數
,這個ajax
函數就是能異步執行的函數,他在執行完了就會調用回調函數
,我承認這個樣做是很容易,早些時候我甚至認為在 javascript 中加了回調函數的函數都可以異步的,異步和回調函數成對出現。多么荒謬的理解啊!
直到有一天,我在寫程序時想到一個問題:在 nodejs 中在不調用系統相關 I/O ,不調用 c++ 寫的 plugin 的情況下,寫一個異步函數?我查了資料,有人給我的答案是調用 setTimeout(fn,delay) 就變成了異步了。但是我還是不明白為什么要調用這樣一個函數,這個函數的語義跟async
完全不一樣,為什么這樣就行?
帶著這個疑問,我查了很多資料,包括官方文檔,代碼,別人的blog。慢慢的理解,最后好像是知道了為什么會是這樣,整篇文章就是對所了解東西的理解。懇請大家批評指正。
說明:nodejs 的文檔是用的 v5.10.1 API,而代碼方面:nodejs 和 libuv 是用的 master 分支。
2 nodejs 的架構基礎
在探索 nodejs 的異步時,首先需要對 nodejs 架構達成統一認識:
- nodejs 有 javascript 的運行環境,目前它的實現是 chrome 的 V8 引擎。
- nodejs 基于事件驅動和非阻塞 I/O 模型,目前它的實現是 libuv。
- 當前的 libuv 是多線程的,文檔中有說明。
- nodejs 在運行時只生成了一個 javascript 運行環境 的實例,也就是說 javascript 解釋器只有一個。
- nodejs 在主線程中調用 V8 引擎的實例執行 javascript 代碼。
如果以上 5 點你不認同的話,那下面就不需要看了,看了會覺得漏洞百出。
上面的 5 點主要說明另一層意思了:
- nodejs 的 javascript 運行環境可以換,在 nodejs 官方 github
中 PR,可以看這個,微軟想把 javascript 運行環境換成自己家的。 - nodejs 的事件驅動和非阻塞 I/O 模型也可以換,目前來看 libuv 運行的不錯,大家都很高興。另外,你可能不知道,chromium 和 chrome 中使用了另一個實現
libevent2
,證據在這里:鏈接。 - nodejs 不是單線程,它是多線程程序,因為 libuv 就已經是多線程了。
- 因為是嵌入式 js 引擎,只能調用宿主環境中提供的方法。當前來說,nodejs 主要把 libuv 的 io/timer 接口提供給了 js 引擎,其他的沒有提供(包括 libuv 的工作線程)。
- nodejs 也沒有提供給 js引擎
新建調用系統線程
的任何方法,所以在nodejs中執行 javascript,是沒有辦法新開線程的。 - js 引擎只有一個實例且在 nodejs 的主線程中調用。
結論
- nodejs 中存在異步,集中在 I/O 和 Timer 調用這一塊,其他的地方沒有。
- js 引擎沒有異步或者并行執行可能,因為 js 引擎是在 nodejs 的主線程調用,所以 js 引擎執行的 javascript 代碼都是同步執行,沒有異步執行。所以你想寫出來一個不調用 I/O和 的異步方法,不可能。
那nodejs中常談的異步
和回調
是怎么回事?
3 nodejs 中的回調和異步的關系是什么?
在 javascript 中使用回調函數
可所謂登峰造極,基本上所有的異步函數都會要求有一個回調函數,以至于寫 javascript 寫多了,看到回調函數的接口,都以為是異步的調用。
但是真相是回調函數
,只是javascript 用來解決異步函數調用如何處理返回值這個問題的方法,或這樣來說:異步函數調用如何處理返回值這個問題上,在系統的設計方面而言,有很多辦法,而 nodejs 選擇了 javascript 的傳統方案,用回調函數來解決這個問題
。
這個選擇好不好,我認為在當時來說,很合適。但隨著 javascript 被用來寫越來越大的程序,這個選擇不是一個好的選擇,因為回調函數嵌套多了真的很難受,我覺得主要是很難看,(就跟 lisp 的 ))))))))))))
),讓一般人不好接受,現在情況改善多了,因為有了Promise。
結論
- 回調函數與異步沒有關系,只是在 javascript 中用來解決異步的返回值的問題,所以異步函數必須帶一個回調函數,他們成對出現,讓人以為有關系。
- 在 javascript 中有回調不一定是異步函數,而異步必須帶一個回調函數。
4 nodejs 中怎樣解決異步的問題?
前面也說了,nodejs 的 js 引擎不能異步執行 javascript 代碼。那js中我們常使用的異步是什么意思的?
答案分為兩部分:
第一部分:與I/O和timer相關的任務,js引擎確實是異步,調用時委托 libuv 進行 I/O 和timer 的相關調用,好了之后就通知 nodejs,nodejs 然后調用 js 引擎執行 javascript 代碼;
第二部分:其它部分的任務,js 引擎把異步
概念(該任務我委托別人執行,我接著執行下面的任務,別人執行完該任務后通知我)弱化成稍后執行
(該任務我委托自己執行但不是現在,我接著執行下面的任務,該任務我稍后
會自己執行,執行完成后通知我自己)的概念。
這就是 js 引擎中異步
的全部意思。基本上等同我們常說的:我馬上做這件事
。不過還是要近一步解釋一下第二部分:
- 任務不能委托給別人,都是自己做。
- 如果當前我做的事件需要很長時間,那我馬上要做的事一直推遲,等我做了完手頭這件事再說。
nodejs 中 js 引擎把異步
變成了稍后執行
,使寫 javascript 程序看起來像異步執行,但是并沒有減少任務,因此在 javascript 中你不能寫一個需要很長時間計算的函數(計算Pi值1000位,大型的矩陣計算),或者在一個tick(后面會說)中執行過多的任務,如果你這樣寫了,整個主線程就沒有辦法響應別的請求,反映出來的情況就是程序卡了
,當然如果非要寫當然也有辦法,需要一些技巧來實現。
而 js 引擎稍后執行
中稍后
到底是多久,到底執行
哪些任務?這些問題就與 nodejs 中四個重要的與時間有關的函數有關了,他們分別是:setTimeout,setInterval,process.nextTick,setImmediate
。下面簡單了解一下這四個函數:
setTimeout 和 setInterval
setImeout 主要是延遲執行函數,其中有一個比較特別的調用:setTimeout(function(){/* code */},0)
,經常見使用,為什么這樣使用看看后面。還有 setInterval 周期性調用一個函數。
setImmediate 和 process.nextTick
setImmediate 的意思翻譯過來是立刻調用的意思,但是官方文檔的解釋是:
Schedules "immediate" execution of callback after I/O events' callbacks and before timers set by setTimeout and setInterval are triggered.
翻譯過來大意就是:被 setImmediate 的設置過的函數,他的執行是在 I/O 事件的回調執行之后,在 計時器觸發的回調執行之前,也就是說在 setTimeout 和 setInterval 之前,好吧這里還有一個順序之分。
process.nextTick 可就更怪了。官方的意思是:
It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.
翻譯過來大意就是:他運行在任何的 I/O 和定時器的 subsequent ticks
之前。
又多了很多的概念,不過別慌,在下面會講 nodejs 的EventLoop,這里講的很多的不理解地方就會在 EventLoop 中講明白。
5 nodejs 中神秘的 EventLoop
EvevtLoop大體上來說就是一個循環,它不停的檢查注冊到他的事件有沒有發生,如果發生了,就執行某些功能,一次循環通常叫tick。這里有講EventLoop,還有這里。
在 nodejs 中也存在這樣一個 EventLoop,不過它是在 libuv 中。它每一次循環叫 tick。而在每一次 tick 中會有不同的階段,每一個階段可以叫 subTick,也就說是這個tick的子tick,libuv就有很多的子 tick,如I/O 和定時器等。下面我用一張圖來表示一下,注意該循環一直在 nodejs 的主線程中運行:
+-------------+
| |
| |
| +-----v----------------------+
| | |
| | uv__update_time(loop) | subTick
| | |
| +-----+----------------------+
| |
| |
| +-----v----------------------+
| | |
| | uv__run_timers(loop) | subTick
| | |
tick| +-----+----------------------+
| |
| |
| +-----v----------------------+
| | |
| | uv__io_poll(loop, timeout) | subTick
| | |
| +-----+----------------------+
| |
| |
| +-----v----------------------+
| | |
| | uv__run_check(loop) | subTick
| | |
| +-----+----------------------+
| |
| |
| |
+-------------+
以上的流程圖已經進行了裁減,只保留重要的內容,如果你想詳細了解,可在 libuv/src/unix/core.cc,第334行:uv_run函數進行詳細了解。
下面來解釋一下各個階段的作用:
uv__update_time
是用來更新定時器
的時間。uv__run_timers
是用來觸發定時器,并執行相關函數的地方。uv__io_poll
是用來 I/O觸發后執行相關函數的地方。
uv__run_check
的用處代碼中講到。
了解到 nodejs 中 EventLoop 的執行階段后,需要更深一步了解在 nodejs 中 js引擎和EvevtLoop是如何被整合在一起工作的。以下是一些偽代碼,它用來說明一些機制。
不過你需要知道在 nodejs 中 setTimeout、setInterval、setImmediate和process.nextTick都是系統級的調用,也就是他們都是c++ 來實現的。setTimeout和setInterval 可看看這個文件:timer_wrap.cc。另外兩個我再補吧。
class V8Engine {
let _jsVM;
V8Engine(){
_jsVM = /*js 執行引擎 */;
}
void invoke(handlers){
// 依次執行,直到 handlers 為空
handlers.forEach(handler,fun => _jsVM.run(handler));
}
}
class EvenLoop {
let _jsRuntime = null;
let _callbackHandlers = []; 【1】
let _processTickHandlers = []; 【2】
let _immediateHandlers = []; 【3】
// 構造函數
EvenLoop(jsRuntime){
_jsRuntime = jsRuntime;
}
void start(){
where(true){
_jsRuntime.invoke(_processTickHandlers); 【4】
_processTickHandlers.clear();
update_time();
run_timer();
run_pool();
run_check();
if (process.exit){
_jsRuntime.invoke(_processTickHandlers); 【5】
_processTickHandlers.clear();
break;
}
}
}
void update_time(){
// 更新 timer 的時間
}
void run_timer(){ 【6】
let handlers = getTimerHandler();
_callbackHandlers.push(handlers);
_jsRuntime.invoke(_callbackHandlers);
_jsRuntime.invoke(_processTickHandlers);
_callbackHandlers.clear();
_processTickHandlers.clear();
}
void run_pool(){ 【6】
let handlers = getIOHandler();
_callbackHandlers.push(handlers);
_jsRuntime.invoke(_callbackHandlers);
_jsRuntime.invoke(_processTickHandlers);
_callbackHandlers.clear();
_processTickHandlers.clear();
}
void run_check(){ 【7】
let handlers = getImmediateHandler();
_immediateHandlers.push(handlers);
_jsRuntime.invoke(_immediateHandlers);
_immediateHandlers.clear();
}
}
main(){
JsRuntime jsRuntime = new V8Engine();
EventLoop eventLoop = new EventLoop(jsRuntime);
eventLoop.start();
}
// 主線程中執行
main();
以上代碼是 nodejs 的粗略的執行過程,還想進一步了解,可以看這從入口函數看起:node_main.cc
按標號進行說明:
- 全局的回調事件先進先出隊列,包括了 I/O 事件和 Timer 事件的回調對象。
- 全局的
nextTick
的回調對象先進先出隊列。 - 全局的
setImmediate
的回調對象先進先出隊列。 - 開始時會執行
nextTick
的隊列。 - 程序退出時會執行
nextTick
的隊列。 - 可以看出
nextTick
隊列會在run_timer
和run_pool
之后執行。回到第三節說的nextTick
的執行時機,看出來該隊列確實會在 I/O 和 Timer 之前運行。在文檔中特別說明如果你遞歸調用nextTick
會阻 I/O 事件的調用就像調用了loop
。依照上面的偽代碼,發現如果你遞歸調用nextTick
,那nextTick
回調對象先進先出隊列就不會為空,js 引擎就一直在執行,影響之后的代碼執行。 -
setImmediate
回調對象先進先出隊列,每一次 tick 就執行一次。
可以從代碼中看出這四個時間函數執行時機的區別,而setTimeout(fn,0)
是在 _callbackHandlers
的隊列中,而setImmediate
,還有 nextTick
都在不同的隊列中執行。
總體來說,nextTick
執行最快,而setTmmediate
能保證每次tick都執行,而setTimeout
是 libuv 的 Timber 保證,可能會有所延遲。
相關鏈接
- 有人覺得得
process.nextTick
名不副實,得改個名字,變成process.currentTick
,沒有通過,理由是太多的代碼依賴這個函數了,沒有辦法改名字,這里。 - 如果你覺得 EventLoop 我說的不清楚,你還可以看看這篇博客:鏈接。
- 如果你覺得 setImmediate 和 nextTick 說的不清楚,可以看這:鏈接。
- 這個也可以:鏈接。
- Synchronously asynchronous。
- designing-apis-for-asynchrony。
6 nodejs 回調和大數據與大計算量的解決方案
回調解決方案- promise
我相信你一但用了promise,你就回不去以往的回調時代,promise 非常好使用,強列推薦使用。如果你還想了解promise怎么實現的,我給你透個底,必不可少setTimeout
這個函數,可以參考 Q promise的設計文檔,還有一步步來手寫一個Promise也不錯。
大數據與大計算量的解決方案 - 分片數據或者分片計算
如果要寫一個處理數據量很大的任務,我想這個函數可以給你思路:
yielding processes
function chunk(array,process,context){
setTimeout(function(){
var item = array.shift();
process.call(context,item);
if (array.length >0){
setTimeout(arguments.callee,100);
}
},100)
}
函數節流
如果要寫一個計算量很大的任務,這個函數也可以給你思路:
var process = {
timeout = null,
// 實際進行處理的方法
performProcessing:function(){
// 實際執行的代碼
},
// 初始處理調用的方法
process:function(){
clearTimeout(this.timeoutId);
var that = this;
this.timeoutId = setTimeout(function(){
that.performProcessing();
},100)
}
}
這兩個函數是從JavaScript高級程序設計第612-615頁摘出來的,本質是不要阻塞了Javascript的事件循環,把任務分片了。
做服務器請求多了,使用 cluster 模塊
cluster 的方案就是多進程方案
。cluster 能包證每個請求被一個 nodejs 實例處理。這樣就能減少每個 nodejs 的處理的數據量。
7 總結
從現在來看 nodejs 架構中對 js 引擎不支持線程調用是一個較大的遺憾,意味著在 nodejs 中你甚至不能做一個很大的計算量的事。不過又說回來,這也是一件好事。因為這樣做的,使 javascript 變簡單,寫 js 不需要考慮鎖的事情,想想在 java 中集合類加鎖,你還要考慮同步,你還要考慮死鎖,我覺得寫 js 的人都很幸福。
其他語言
同樣的問題也出現在 python、ruby 和 php 上。這些語言在當前的主流版本(用c實現的版本)中都默認一把大鎖 GIL,所有的代碼都是主線程中運行,代碼都是線程安全的,基本上第三方庫也利用這個現實。導致的事實是它們都沒有辦法很好的利用現在的多核計算機,多么悲劇的事情啊!
不過好在,計算這事情,它們干不了,還有人來干,就是老大哥 c、c++還有 java 了。你沒有看到分布式計算領域和大數據中核心計算被老大哥占領,其他是想占也占不了,不是不想占,是有心無力。
就目前的分析,我覺得這篇文章說的很對。
未來發展
當前 nodejs 的發展還是在填別的語言中經歷過的坑,因為 nodejs 發展畢竟才七年的時間(2009年建立),流行也才是近幾年的事情。不過 nodejs 的進步很快(后發優勢),做一個輕量級的網頁應用已經是繼 python、ruby、php之后的另一個選擇了,可喜可賀。
但是如果還要更近一步發展,那就必須解決計算這個問題。當前 javascript 對于這個問題的解決基本還是按著沿用 python、ruby 和 php 走過的路線走下去,采用單線程協程
的方案,也就是 yield、async/wait 方案。在這之后,也基本上會采用多線程方案 worker 。從這樣的發展來看,未來的 nodejs 與 python、ruby、php 是并駕齊驅的解決方案,不見得比 python、ruby 和 php 更好,它們都差不多,唯一不同的是我們又多了一種選擇而已。
想到程序員在論壇上問:新手學習網站開發,javacript、python、ruby和 php 哪個好?我想說如果有師博他說什么好就學什么,如果沒有師博那就學 javascript 吧,因為你不用再去學一門后端的語言了。