nodejs中異步

nodejs中的異步

1 nodejs 中的異步存在嗎?

如今有點 javascript 基礎的人都在據說過 nodejs ,而只要與 javascript 打交到人都會用或者是將要使用 nodejs 。畢竟 nodejs 的生態很強大,與 javascript 相關的工具也作的很方便,很好用。javascript

javascript 語言很小巧,可是一旦與 nodejs 中的運行環境放在一塊兒,有些概念就很難理解,特別是異步的概念。有人會說不會啊,很好理解啊?不就是一個ajax請求加上一個回調函數,這個ajax函數就是能異步執行的函數,他在執行完了就會調用回調函數,我認可這個樣作是很容易,早些時候我甚至認爲在 javascript 中加了回調函數的函數均可以異步的,異步和回調函數成對出現。多麼荒謬的理解啊!php

直到有一天,我在寫程序時想到一個問題:在 nodejs 中在不調用系統相關 I/O ,不調用 c++ 寫的 plugin 的狀況下,寫一個異步函數?我查了資料,有人給個人答案是調用 setTimeout(fn,delay) 就變成了異步了。可是我仍是不明白爲何要調用這樣一個函數,這個函數的語義跟async徹底不同,爲何這樣就行?html

帶着這個疑問,我查了不少資料,包括官方文檔,代碼,別人的blog。慢慢的理解,最後好像是知道了爲何會是這樣,整篇文章就是對所瞭解東西的理解。懇請你們批評指正。java

說明:nodejs 的文檔是用的 v5.10.1 API,而代碼方面:nodejs 和 libuv 是用的 master 分支。node

2 nodejs 的架構基礎

2.1 基礎

在探索 nodejs 的異步時,首先須要對 nodejs 架構達成統一認識:python

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

若是以上 5 點你不認同的話,那下面就不須要看了,看了會以爲漏洞百出。c++

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

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

2.2 結論

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

那nodejs中常談的異步回調是怎麼回事?程序員

3 nodejs 中的回調和異步的關係是什麼?

3.1 爲何是回調

在 javascript 中使用回調函數可所謂登峯造極,基本上全部的異步函數都會要求有一個回調函數,以致於寫 javascript 寫多了,看到回調函數的接口,都覺得是異步的調用。github

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

這個選擇好很差,我認爲在當時來講,很合適。但隨着 javascript 被用來寫愈來愈大的程序,這個選擇不是一個好的選擇,由於回調函數嵌套多了真的很難受,我以爲主要是很難看,(就跟 lisp 的 )))))))))))) ),讓通常人很差接受,如今狀況改善多了,由於有了Promise。

3.2 結論

  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。下面簡單瞭解一下這四個函數:

4.1 setTimeout 和 setInterval

setImeout 主要是延遲執行函數,其中有一個比較特別的調用:setTimeout(function(){/* code */},0),常常見使用,爲何這樣使用看看後面。還有 setInterval 週期性調用一個函數。

4.2 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
  7. ***** javaScript 運行機制詳解:再談Event Loop 的博客真的很容易懂
  8. nodejs真的是單線程嗎?,這篇文章講的不錯。

6 nodejs 回調和大數據與大計算量的解決方案

6.1 回調解決方案- promise

我相信你一但用了promise,你就回不去以往的回調時代,promise 很是好使用,強列推薦使用。若是你還想了解promise怎麼實現的,我給你透個底,必不可少setTimeout這個函數,能夠參考 Q promise的設計文檔,還有一步步來手寫一個Promise也不錯。

6.2 大數據與大計算量的解決方案 - 分片數據或者分片計算

若是要寫一個處理數據量很大的任務,我想這個函數能夠給你思路:

6.2.1 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)
}

6.2.2 函數節流

若是要寫一個計算量很大的任務,這個函數也能夠給你思路:

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的事件循環,把任務分片了。

6.3 負載

作服務器請求多了,使用 cluster 模塊。cluster 的方案就是nodejs的多進程方案。cluster 能保證每一個請求被一個 nodejs 實例處理。這樣就能減小每一個 nodejs 的處理的數據量。

7 總結

從如今來看 nodejs 架構中對 js 引擎不支持線程調用是一個較大的遺憾,意味着在 nodejs 中你甚至不能作一個很大的計算量的事。不過又說回來,這也是一件好事。由於這樣作的,使 javascript 變簡單,寫 js 不須要考慮鎖的事情,想一想在 java 中集合類加鎖,你還要考慮同步,你還要考慮死鎖,我以爲寫 js 的人都很幸福。

7.1 其餘語言

一樣的問題也出如今 python、ruby 和 php 上。這些語言在當前的主流版本(用c實現的版本)中都默認一把大鎖 GIL,全部的代碼都是主線程中運行,代碼都是線程安全的,基本上第三方庫也利用這個現實。致使的事實是它們都沒有辦法很好的利用如今的多核計算機,多麼悲劇的事情啊!

不過好在,計算這事情,它們幹不了,還有人來幹,就是老大哥 c、c++還有 java 了。你沒有看到分佈式計算領域和大數據中核心計算被老大哥佔領,其餘是想佔也佔不了,不是不想佔,是有心無力。

就目前的分析,我以爲這篇文章說的很對。

7.2 將來發展

當前 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 吧,由於你不用再去學一門後端的語言了。

相關文章
相關標籤/搜索