nodejs中異步

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 架構達成統一認識:

  1. nodejs 有 javascript 的運行環境,目前它的實現是 chrome 的 V8 引擎。
  2. nodejs 基于事件驅動和非阻塞 I/O 模型,目前它的實現是 libuv。
  3. 當前的 libuv 是多線程的,文檔中有說明。
  4. nodejs 在運行時只生成了一個 javascript 運行環境 的實例,也就是說 javascript 解釋器只有一個。
  5. nodejs 在主線程中調用 V8 引擎的實例執行 javascript 代碼。

如果以上 5 點你不認同的話,那下面就不需要看了,看了會覺得漏洞百出。

上面的 5 點主要說明另一層意思了:

  1. nodejs 的 javascript 運行環境可以換,在 nodejs 官方 github
    中 PR,可以看這個,微軟想把 javascript 運行環境換成自己家的。
  2. nodejs 的事件驅動和非阻塞 I/O 模型也可以換,目前來看 libuv 運行的不錯,大家都很高興。另外,你可能不知道,chromium 和 chrome 中使用了另一個實現 libevent2,證據在這里:鏈接
  3. nodejs 不是單線程,它是多線程程序,因為 libuv 就已經是多線程了。
  4. 因為是嵌入式 js 引擎,只能調用宿主環境中提供的方法。當前來說,nodejs 主要把 libuv 的 io/timer 接口提供給了 js 引擎,其他的沒有提供(包括 libuv 的工作線程)。
  5. nodejs 也沒有提供給 js引擎 新建調用系統線程的任何方法,所以在nodejs中執行 javascript,是沒有辦法新開線程的。
  6. js 引擎只有一個實例且在 nodejs 的主線程中調用。

結論

  1. nodejs 中存在異步,集中在 I/O 和 Timer 調用這一塊,其他的地方沒有。
  2. js 引擎沒有異步或者并行執行可能,因為 js 引擎是在 nodejs 的主線程調用,所以 js 引擎執行的 javascript 代碼都是同步執行,沒有異步執行。所以你想寫出來一個不調用 I/O和 的異步方法,不可能。

那nodejs中常談的異步回調是怎么回事?

3 nodejs 中的回調和異步的關系是什么?

在 javascript 中使用回調函數可所謂登峰造極,基本上所有的異步函數都會要求有一個回調函數,以至于寫 javascript 寫多了,看到回調函數的接口,都以為是異步的調用。

但是真相是回調函數,只是javascript 用來解決異步函數調用如何處理返回值這個問題的方法,或這樣來說:異步函數調用如何處理返回值這個問題上,在系統的設計方面而言,有很多辦法,而 nodejs 選擇了 javascript 的傳統方案,用回調函數來解決這個問題

這個選擇好不好,我認為在當時來說,很合適。但隨著 javascript 被用來寫越來越大的程序,這個選擇不是一個好的選擇,因為回調函數嵌套多了真的很難受,我覺得主要是很難看,(就跟 lisp 的 )))))))))))) ),讓一般人不好接受,現在情況改善多了,因為有了Promise。

結論

  1. 回調函數與異步沒有關系,只是在 javascript 中用來解決異步的返回值的問題,所以異步函數必須帶一個回調函數,他們成對出現,讓人以為有關系。
  2. 在 javascript 中有回調不一定是異步函數,而異步必須帶一個回調函數。

4 nodejs 中怎樣解決異步的問題?

前面也說了,nodejs 的 js 引擎不能異步執行 javascript 代碼。那js中我們常使用的異步是什么意思的?

答案分為兩部分:

第一部分:與I/O和timer相關的任務,js引擎確實是異步,調用時委托 libuv 進行 I/O 和timer 的相關調用,好了之后就通知 nodejs,nodejs 然后調用 js 引擎執行 javascript 代碼;

第二部分:其它部分的任務,js 引擎把異步概念(該任務我委托別人執行,我接著執行下面的任務,別人執行完該任務后通知我)弱化成稍后執行(該任務我委托自己執行但不是現在,我接著執行下面的任務,該任務我稍后會自己執行,執行完成后通知我自己)的概念。

這就是 js 引擎中異步的全部意思。基本上等同我們常說的:我馬上做這件事。不過還是要近一步解釋一下第二部分:

  1. 任務不能委托給別人,都是自己做。
  2. 如果當前我做的事件需要很長時間,那我馬上要做的事一直推遲,等我做了完手頭這件事再說。

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

按標號進行說明:

  1. 全局的回調事件先進先出隊列,包括了 I/O 事件和 Timer 事件的回調對象。
  2. 全局的nextTick的回調對象先進先出隊列。
  3. 全局的setImmediate的回調對象先進先出隊列。
  4. 開始時會執行 nextTick的隊列。
  5. 程序退出時會執行 nextTick的隊列。
  6. 可以看出nextTick隊列會在run_timerrun_pool之后執行。回到第三節說的nextTick的執行時機,看出來該隊列確實會在 I/O 和 Timer 之前運行。在文檔中特別說明如果你遞歸調用 nextTick 會阻 I/O 事件的調用就像調用了 loop。依照上面的偽代碼,發現如果你遞歸調用nextTick,那nextTick回調對象先進先出隊列就不會為空,js 引擎就一直在執行,影響之后的代碼執行。
  7. setImmediate 回調對象先進先出隊列,每一次 tick 就執行一次。

可以從代碼中看出這四個時間函數執行時機的區別,而setTimeout(fn,0)是在 _callbackHandlers的隊列中,而setImmediate,還有 nextTick 都在不同的隊列中執行。

總體來說,nextTick執行最快,而setTmmediate能保證每次tick都執行,而setTimeout是 libuv 的 Timber 保證,可能會有所延遲。

相關鏈接

  1. 有人覺得得 process.nextTick 名不副實,得改個名字,變成 process.currentTick,沒有通過,理由是太多的代碼依賴這個函數了,沒有辦法改名字,這里
  2. 如果你覺得 EventLoop 我說的不清楚,你還可以看看這篇博客:鏈接
  3. 如果你覺得 setImmediate 和 nextTick 說的不清楚,可以看這:鏈接
  4. 這個也可以:鏈接
  5. Synchronously asynchronous
  6. 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 走過的路線走下去,采用單線程協程的方案,也就是 yieldasync/wait 方案。在這之后,也基本上會采用多線程方案 worker 。從這樣的發展來看,未來的 nodejs 與 python、ruby、php 是并駕齊驅的解決方案,不見得比 python、ruby 和 php 更好,它們都差不多,唯一不同的是我們又多了一種選擇而已。

想到程序員在論壇上問:新手學習網站開發,javacript、python、ruby和 php 哪個好?我想說如果有師博他說什么好就學什么,如果沒有師博那就學 javascript 吧,因為你不用再去學一門后端的語言了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容