1 nodejs 中的異步存在嗎伤为?
現(xiàn)在有點(diǎn) javascript 基礎(chǔ)的人都在聽說過 nodejs ,而只要與 javascript 打交到人都會用或者是將要使用 nodejs 据途。畢竟 nodejs 的生態(tài)很強(qiáng)大绞愚,與 javascript 相關(guān)的工具也做的很方便,很好用颖医。
javascript 語言很小巧位衩,但是一旦與 nodejs 中的運(yùn)行環(huán)境放在一起,有些概念就很難理解熔萧,特別是異步
的概念糖驴。有人會說不會啊僚祷,很好理解啊贮缕?不就是一個ajax
請求加上一個回調(diào)函數(shù)
辙谜,這個ajax
函數(shù)就是能異步執(zhí)行的函數(shù),他在執(zhí)行完了就會調(diào)用回調(diào)函數(shù)
感昼,我承認(rèn)這個樣做是很容易装哆,早些時候我甚至認(rèn)為在 javascript 中加了回調(diào)函數(shù)的函數(shù)都可以異步的,異步和回調(diào)函數(shù)成對出現(xiàn)定嗓。多么荒謬的理解巴汕佟!
直到有一天宵溅,我在寫程序時想到一個問題:在 nodejs 中在不調(diào)用系統(tǒng)相關(guān) I/O 凌简,不調(diào)用 c++ 寫的 plugin 的情況下,寫一個異步函數(shù)恃逻?我查了資料雏搂,有人給我的答案是調(diào)用 setTimeout(fn,delay) 就變成了異步了。但是我還是不明白為什么要調(diào)用這樣一個函數(shù)寇损,這個函數(shù)的語義跟async
完全不一樣畔派,為什么這樣就行?
帶著這個疑問润绵,我查了很多資料,包括官方文檔胞谈,代碼尘盼,別人的blog。慢慢的理解烦绳,最后好像是知道了為什么會是這樣卿捎,整篇文章就是對所了解東西的理解。懇請大家批評指正径密。
說明:nodejs 的文檔是用的 v5.10.1 API午阵,而代碼方面:nodejs 和 libuv 是用的 master 分支。
2 nodejs 的架構(gòu)基礎(chǔ)
在探索 nodejs 的異步時享扔,首先需要對 nodejs 架構(gòu)達(dá)成統(tǒng)一認(rèn)識:
- nodejs 有 javascript 的運(yùn)行環(huán)境底桂,目前它的實(shí)現(xiàn)是 chrome 的 V8 引擎。
- nodejs 基于事件驅(qū)動和非阻塞 I/O 模型惧眠,目前它的實(shí)現(xiàn)是 libuv籽懦。
- 當(dāng)前的 libuv 是多線程的,文檔中有說明氛魁。
- nodejs 在運(yùn)行時只生成了一個 javascript 運(yùn)行環(huán)境 的實(shí)例暮顺,也就是說 javascript 解釋器只有一個厅篓。
- nodejs 在主線程中調(diào)用 V8 引擎的實(shí)例執(zhí)行 javascript 代碼。
如果以上 5 點(diǎn)你不認(rèn)同的話捶码,那下面就不需要看了羽氮,看了會覺得漏洞百出。
上面的 5 點(diǎn)主要說明另一層意思了:
- nodejs 的 javascript 運(yùn)行環(huán)境可以換惫恼,在 nodejs 官方 github
中 PR档押,可以看這個,微軟想把 javascript 運(yùn)行環(huán)境換成自己家的尤筐。 - nodejs 的事件驅(qū)動和非阻塞 I/O 模型也可以換汇荐,目前來看 libuv 運(yùn)行的不錯,大家都很高興盆繁。另外掀淘,你可能不知道,chromium 和 chrome 中使用了另一個實(shí)現(xiàn)
libevent2
油昂,證據(jù)在這里:鏈接革娄。 - nodejs 不是單線程,它是多線程程序冕碟,因?yàn)?libuv 就已經(jīng)是多線程了拦惋。
- 因?yàn)槭乔度胧?js 引擎,只能調(diào)用宿主環(huán)境中提供的方法安寺。當(dāng)前來說厕妖,nodejs 主要把 libuv 的 io/timer 接口提供給了 js 引擎,其他的沒有提供(包括 libuv 的工作線程)挑庶。
- nodejs 也沒有提供給 js引擎
新建調(diào)用系統(tǒng)線程
的任何方法言秸,所以在nodejs中執(zhí)行 javascript,是沒有辦法新開線程的迎捺。 - js 引擎只有一個實(shí)例且在 nodejs 的主線程中調(diào)用举畸。
結(jié)論
- nodejs 中存在異步,集中在 I/O 和 Timer 調(diào)用這一塊凳枝,其他的地方?jīng)]有抄沮。
- js 引擎沒有異步或者并行執(zhí)行可能,因?yàn)?js 引擎是在 nodejs 的主線程調(diào)用岖瑰,所以 js 引擎執(zhí)行的 javascript 代碼都是同步執(zhí)行叛买,沒有異步執(zhí)行。所以你想寫出來一個不調(diào)用 I/O和 的異步方法蹋订,不可能聪全。
那nodejs中常談的異步
和回調(diào)
是怎么回事?
3 nodejs 中的回調(diào)和異步的關(guān)系是什么辅辩?
在 javascript 中使用回調(diào)函數(shù)
可所謂登峰造極难礼,基本上所有的異步函數(shù)都會要求有一個回調(diào)函數(shù)娃圆,以至于寫 javascript 寫多了,看到回調(diào)函數(shù)的接口蛾茉,都以為是異步的調(diào)用讼呢。
但是真相是回調(diào)函數(shù)
,只是javascript 用來解決異步函數(shù)調(diào)用如何處理返回值這個問題的方法谦炬,或這樣來說:異步函數(shù)調(diào)用如何處理返回值這個問題上悦屏,在系統(tǒng)的設(shè)計(jì)方面而言,有很多辦法键思,而 nodejs 選擇了 javascript 的傳統(tǒng)方案础爬,用回調(diào)函數(shù)來解決這個問題
。
這個選擇好不好吼鳞,我認(rèn)為在當(dāng)時來說看蚜,很合適。但隨著 javascript 被用來寫越來越大的程序赔桌,這個選擇不是一個好的選擇供炎,因?yàn)榛卣{(diào)函數(shù)嵌套多了真的很難受,我覺得主要是很難看疾党,(就跟 lisp 的 ))))))))))))
)音诫,讓一般人不好接受,現(xiàn)在情況改善多了雪位,因?yàn)橛辛薖romise竭钝。
結(jié)論
- 回調(diào)函數(shù)與異步?jīng)]有關(guān)系,只是在 javascript 中用來解決異步的返回值的問題雹洗,所以異步函數(shù)必須帶一個回調(diào)函數(shù)蜓氨,他們成對出現(xiàn),讓人以為有關(guān)系队伟。
- 在 javascript 中有回調(diào)不一定是異步函數(shù),而異步必須帶一個回調(diào)函數(shù)幽勒。
4 nodejs 中怎樣解決異步的問題嗜侮?
前面也說了,nodejs 的 js 引擎不能異步執(zhí)行 javascript 代碼啥容。那js中我們常使用的異步是什么意思的锈颗?
答案分為兩部分:
第一部分:與I/O和timer相關(guān)的任務(wù),js引擎確實(shí)是異步咪惠,調(diào)用時委托 libuv 進(jìn)行 I/O 和timer 的相關(guān)調(diào)用击吱,好了之后就通知 nodejs,nodejs 然后調(diào)用 js 引擎執(zhí)行 javascript 代碼遥昧;
第二部分:其它部分的任務(wù)覆醇,js 引擎把異步
概念(該任務(wù)我委托別人執(zhí)行朵纷,我接著執(zhí)行下面的任務(wù),別人執(zhí)行完該任務(wù)后通知我)弱化成稍后執(zhí)行
(該任務(wù)我委托自己執(zhí)行但不是現(xiàn)在永脓,我接著執(zhí)行下面的任務(wù)袍辞,該任務(wù)我稍后
會自己執(zhí)行,執(zhí)行完成后通知我自己)的概念常摧。
這就是 js 引擎中異步
的全部意思搅吁。基本上等同我們常說的:我馬上做這件事
落午。不過還是要近一步解釋一下第二部分:
- 任務(wù)不能委托給別人谎懦,都是自己做。
- 如果當(dāng)前我做的事件需要很長時間溃斋,那我馬上要做的事一直推遲界拦,等我做了完手頭這件事再說。
nodejs 中 js 引擎把異步
變成了稍后執(zhí)行
盐类,使寫 javascript 程序看起來像異步執(zhí)行寞奸,但是并沒有減少任務(wù),因此在 javascript 中你不能寫一個需要很長時間計(jì)算的函數(shù)(計(jì)算Pi值1000位在跳,大型的矩陣計(jì)算)枪萄,或者在一個tick(后面會說)中執(zhí)行過多的任務(wù),如果你這樣寫了猫妙,整個主線程就沒有辦法響應(yīng)別的請求瓷翻,反映出來的情況就是程序卡了
,當(dāng)然如果非要寫當(dāng)然也有辦法割坠,需要一些技巧來實(shí)現(xiàn)齐帚。
而 js 引擎稍后執(zhí)行
中稍后
到底是多久,到底執(zhí)行
哪些任務(wù)彼哼?這些問題就與 nodejs 中四個重要的與時間有關(guān)的函數(shù)有關(guān)了对妄,他們分別是:setTimeout,setInterval敢朱,process.nextTick剪菱,setImmediate
。下面簡單了解一下這四個函數(shù):
setTimeout 和 setInterval
setImeout 主要是延遲執(zhí)行函數(shù)拴签,其中有一個比較特別的調(diào)用:setTimeout(function(){/* code */},0)
孝常,經(jīng)常見使用,為什么這樣使用看看后面蚓哩。還有 setInterval 周期性調(diào)用一個函數(shù)构灸。
setImmediate 和 process.nextTick
setImmediate 的意思翻譯過來是立刻調(diào)用的意思,但是官方文檔的解釋是:
Schedules "immediate" execution of callback after I/O events' callbacks and before timers set by setTimeout and setInterval are triggered.
翻譯過來大意就是:被 setImmediate 的設(shè)置過的函數(shù)岸梨,他的執(zhí)行是在 I/O 事件的回調(diào)執(zhí)行之后喜颁,在 計(jì)時器觸發(fā)的回調(diào)執(zhí)行之前稠氮,也就是說在 setTimeout 和 setInterval 之前,好吧這里還有一個順序之分洛巢。
process.nextTick 可就更怪了括袒。官方的意思是:
It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.
翻譯過來大意就是:他運(yùn)行在任何的 I/O 和定時器的 subsequent ticks
之前。
又多了很多的概念稿茉,不過別慌锹锰,在下面會講 nodejs 的EventLoop,這里講的很多的不理解地方就會在 EventLoop 中講明白漓库。
5 nodejs 中神秘的 EventLoop
EvevtLoop大體上來說就是一個循環(huán)恃慧,它不停的檢查注冊到他的事件有沒有發(fā)生,如果發(fā)生了渺蒿,就執(zhí)行某些功能痢士,一次循環(huán)通常叫tick。這里有講EventLoop茂装,還有這里怠蹂。
在 nodejs 中也存在這樣一個 EventLoop,不過它是在 libuv 中少态。它每一次循環(huán)叫 tick城侧。而在每一次 tick 中會有不同的階段,每一個階段可以叫 subTick彼妻,也就說是這個tick的子tick嫌佑,libuv就有很多的子 tick,如I/O 和定時器等侨歉。下面我用一張圖來表示一下屋摇,注意該循環(huán)一直在 nodejs 的主線程中運(yùn)行:
+-------------+
| |
| |
| +-----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
| | |
| +-----+----------------------+
| |
| |
| |
+-------------+
以上的流程圖已經(jīng)進(jìn)行了裁減,只保留重要的內(nèi)容幽邓,如果你想詳細(xì)了解炮温,可在 libuv/src/unix/core.cc,第334行:uv_run函數(shù)進(jìn)行詳細(xì)了解牵舵。
下面來解釋一下各個階段的作用:
uv__update_time
是用來更新定時器
的時間柒啤。uv__run_timers
是用來觸發(fā)定時器,并執(zhí)行相關(guān)函數(shù)的地方棋枕。uv__io_poll
是用來 I/O觸發(fā)后執(zhí)行相關(guān)函數(shù)的地方。
uv__run_check
的用處代碼中講到妒峦。
了解到 nodejs 中 EventLoop 的執(zhí)行階段后重斑,需要更深一步了解在 nodejs 中 js引擎和EvevtLoop是如何被整合在一起工作的。以下是一些偽代碼肯骇,它用來說明一些機(jī)制窥浪。
不過你需要知道在 nodejs 中 setTimeout祖很、setInterval、setImmediate和process.nextTick都是系統(tǒng)級的調(diào)用漾脂,也就是他們都是c++ 來實(shí)現(xiàn)的假颇。setTimeout和setInterval 可看看這個文件:timer_wrap.cc。另外兩個我再補(bǔ)吧骨稿。
class V8Engine {
let _jsVM;
V8Engine(){
_jsVM = /*js 執(zhí)行引擎 */;
}
void invoke(handlers){
// 依次執(zhí)行笨鸡,直到 handlers 為空
handlers.forEach(handler,fun => _jsVM.run(handler));
}
}
class EvenLoop {
let _jsRuntime = null;
let _callbackHandlers = []; 【1】
let _processTickHandlers = []; 【2】
let _immediateHandlers = []; 【3】
// 構(gòu)造函數(shù)
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();
}
// 主線程中執(zhí)行
main();
以上代碼是 nodejs 的粗略的執(zhí)行過程,還想進(jìn)一步了解坦冠,可以看這從入口函數(shù)看起:node_main.cc
按標(biāo)號進(jìn)行說明:
- 全局的回調(diào)事件先進(jìn)先出隊(duì)列形耗,包括了 I/O 事件和 Timer 事件的回調(diào)對象。
- 全局的
nextTick
的回調(diào)對象先進(jìn)先出隊(duì)列辙浑。 - 全局的
setImmediate
的回調(diào)對象先進(jìn)先出隊(duì)列激涤。 - 開始時會執(zhí)行
nextTick
的隊(duì)列。 - 程序退出時會執(zhí)行
nextTick
的隊(duì)列判呕。 - 可以看出
nextTick
隊(duì)列會在run_timer
和run_pool
之后執(zhí)行倦踢√钐В回到第三節(jié)說的nextTick
的執(zhí)行時機(jī)癌淮,看出來該隊(duì)列確實(shí)會在 I/O 和 Timer 之前運(yùn)行。在文檔中特別說明如果你遞歸調(diào)用nextTick
會阻 I/O 事件的調(diào)用就像調(diào)用了loop
鳍贾。依照上面的偽代碼梦抢,發(fā)現(xiàn)如果你遞歸調(diào)用nextTick
,那nextTick
回調(diào)對象先進(jìn)先出隊(duì)列就不會為空般贼,js 引擎就一直在執(zhí)行,影響之后的代碼執(zhí)行奥吩。 -
setImmediate
回調(diào)對象先進(jìn)先出隊(duì)列哼蛆,每一次 tick 就執(zhí)行一次。
可以從代碼中看出這四個時間函數(shù)執(zhí)行時機(jī)的區(qū)別霞赫,而setTimeout(fn,0)
是在 _callbackHandlers
的隊(duì)列中腮介,而setImmediate
,還有 nextTick
都在不同的隊(duì)列中執(zhí)行端衰。
總體來說叠洗,nextTick
執(zhí)行最快,而setTmmediate
能保證每次tick都執(zhí)行旅东,而setTimeout
是 libuv 的 Timber 保證灭抑,可能會有所延遲。
相關(guān)鏈接
- 有人覺得得
process.nextTick
名不副實(shí)抵代,得改個名字腾节,變成process.currentTick
,沒有通過,理由是太多的代碼依賴這個函數(shù)了案腺,沒有辦法改名字庆冕,這里。 - 如果你覺得 EventLoop 我說的不清楚劈榨,你還可以看看這篇博客:鏈接访递。
- 如果你覺得 setImmediate 和 nextTick 說的不清楚,可以看這:鏈接同辣。
- 這個也可以:鏈接拷姿。
- Synchronously asynchronous。
- designing-apis-for-asynchrony邑闺。
6 nodejs 回調(diào)和大數(shù)據(jù)與大計(jì)算量的解決方案
回調(diào)解決方案- promise
我相信你一但用了promise跌前,你就回不去以往的回調(diào)時代,promise 非常好使用陡舅,強(qiáng)列推薦使用抵乓。如果你還想了解promise怎么實(shí)現(xiàn)的,我給你透個底靶衍,必不可少setTimeout
這個函數(shù)灾炭,可以參考 Q promise的設(shè)計(jì)文檔,還有一步步來手寫一個Promise也不錯颅眶。
大數(shù)據(jù)與大計(jì)算量的解決方案 - 分片數(shù)據(jù)或者分片計(jì)算
如果要寫一個處理數(shù)據(jù)量很大的任務(wù)蜈出,我想這個函數(shù)可以給你思路:
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)
}
函數(shù)節(jié)流
如果要寫一個計(jì)算量很大的任務(wù)涛酗,這個函數(shù)也可以給你思路:
var process = {
timeout = null,
// 實(shí)際進(jìn)行處理的方法
performProcessing:function(){
// 實(shí)際執(zhí)行的代碼
},
// 初始處理調(diào)用的方法
process:function(){
clearTimeout(this.timeoutId);
var that = this;
this.timeoutId = setTimeout(function(){
that.performProcessing();
},100)
}
}
這兩個函數(shù)是從JavaScript高級程序設(shè)計(jì)第612-615頁摘出來的铡原,本質(zhì)是不要阻塞了Javascript的事件循環(huán),把任務(wù)分片了商叹。
做服務(wù)器請求多了燕刻,使用 cluster 模塊
cluster 的方案就是多進(jìn)程方案
。cluster 能包證每個請求被一個 nodejs 實(shí)例處理剖笙。這樣就能減少每個 nodejs 的處理的數(shù)據(jù)量卵洗。
7 總結(jié)
從現(xiàn)在來看 nodejs 架構(gòu)中對 js 引擎不支持線程調(diào)用是一個較大的遺憾,意味著在 nodejs 中你甚至不能做一個很大的計(jì)算量的事弥咪。不過又說回來过蹂,這也是一件好事。因?yàn)檫@樣做的聚至,使 javascript 變簡單酷勺,寫 js 不需要考慮鎖的事情,想想在 java 中集合類加鎖扳躬,你還要考慮同步脆诉,你還要考慮死鎖勋功,我覺得寫 js 的人都很幸福。
其他語言
同樣的問題也出現(xiàn)在 python库说、ruby 和 php 上。這些語言在當(dāng)前的主流版本(用c實(shí)現(xiàn)的版本)中都默認(rèn)一把大鎖 GIL片择,所有的代碼都是主線程中運(yùn)行潜的,代碼都是線程安全的,基本上第三方庫也利用這個現(xiàn)實(shí)字管。導(dǎo)致的事實(shí)是它們都沒有辦法很好的利用現(xiàn)在的多核計(jì)算機(jī)啰挪,多么悲劇的事情啊嘲叔!
不過好在亡呵,計(jì)算這事情,它們干不了硫戈,還有人來干锰什,就是老大哥 c、c++還有 java 了丁逝。你沒有看到分布式計(jì)算領(lǐng)域和大數(shù)據(jù)中核心計(jì)算被老大哥占領(lǐng)汁胆,其他是想占也占不了,不是不想占霜幼,是有心無力嫩码。
就目前的分析,我覺得這篇文章說的很對罪既。
未來發(fā)展
當(dāng)前 nodejs 的發(fā)展還是在填別的語言中經(jīng)歷過的坑铸题,因?yàn)?nodejs 發(fā)展畢竟才七年的時間(2009年建立),流行也才是近幾年的事情琢感。不過 nodejs 的進(jìn)步很快(后發(fā)優(yōu)勢)丢间,做一個輕量級的網(wǎng)頁應(yīng)用已經(jīng)是繼 python、ruby猩谊、php之后的另一個選擇了千劈,可喜可賀。
但是如果還要更近一步發(fā)展牌捷,那就必須解決計(jì)算這個問題墙牌。當(dāng)前 javascript 對于這個問題的解決基本還是按著沿用 python、ruby 和 php 走過的路線走下去暗甥,采用單線程協(xié)程
的方案喜滨,也就是 yield、async/wait 方案撤防。在這之后虽风,也基本上會采用多線程方案 worker 。從這樣的發(fā)展來看,未來的 nodejs 與 python辜膝、ruby无牵、php 是并駕齊驅(qū)的解決方案,不見得比 python茎毁、ruby 和 php 更好,它們都差不多忱辅,唯一不同的是我們又多了一種選擇而已七蜘。
想到程序員在論壇上問:新手學(xué)習(xí)網(wǎng)站開發(fā),javacript墙懂、python橡卤、ruby和 php 哪個好?我想說如果有師博他說什么好就學(xué)什么损搬,如果沒有師博那就學(xué) javascript 吧碧库,因?yàn)槟悴挥迷偃W(xué)一門后端的語言了。