Node.js理解四個關於高併發的四個問題

1、爲什麼在瀏覽器中運行的javaScript能與操作系統進行如此底層的交互?
2、Node.js真的是單線程嗎?
3、如果是單線程,他是如何處理高併發請求的?
4、node.js事件驅動是如何實現的?

第一問題:
在這裏插入圖片描述
· Node.js標準庫,這部分是有JAVASCRIPT 編寫的,即使我們使用過程中能直接能調用的API.在源碼中的LIB目錄中可以看到。
·Node bindings, 這一層是JavaScript與底層C/C++能夠交流的關鍵,前者通過bindings調用後者,相互交換數據。實現在node.cc
·這一層是支撐Node.js運行的關鍵,由C/C++實現。
V8:Google推出的Javascript VM ,也是Node.js爲什麼使用的是Javascript的關鍵,他爲js提供了非瀏覽器的運行環境。
Libuv:它爲Node.js提供了跨平臺,線程池,事件池,異步I/O等能力,是Node.js如此強大的關鍵。
C-ares:提供了一部處理的DNS相關的能力。
http_parser、Open SSL、zlib等:提供了包括http解析、SSL、數據壓縮等其他能力

在這裏插入圖片描述
具體來說,當我們調用 fs.open 時,Node.js 通過 process.binding 調用 C/C++ 層面的 Open 函數,然後通過它調用 Libuv 中的具體方法 uv_fs_open,最後執行的結果通過回調的方式傳回,完成流程。

我們在 Javascript中調用的方法,最終都會通過 process.binding 傳遞到 C/C++ 層面,最終由他們來執行真正的操作。Node.js 即這樣與操作系統進行互動。

第二問題:
在傳統web 服務模型中,大多都使用多線程來解決併發的問題,因爲I/O 是阻塞的,單線程就意味着用戶要等待,顯然這是不合理的,所以創建多個線程來響應用戶的請求。
Node.js 對http 服務的模型:
在這裏插入圖片描述

Node.js的單線程指的是主線程是“單線程”,由主要線程去按照編碼順序一步步執行程序代碼,假如遇到同步代碼阻塞,主線程被佔用,後續的程序代碼執行就會被卡住。實踐一個測試代碼:

var http = require(‘http’);

function sleep(time) {
var _exit = Date.now() + time * 1000;
while( Date.now() < _exit ) {}
return ;
}

var server = http.createServer(function(req, res){
sleep(10);
res.end(‘server sleep 10s’);
});

server.listen(8080);
下面爲代碼塊的堆棧圖:
加粗樣式
先將index.js的代碼改成這樣,然後打開瀏覽器,你會發現瀏覽器在10秒之後才做出反應,打出Hello Node.js。

JavaScript是解析性語言,代碼按照編碼順序一行一行被壓進stack裏面執行,執行完成後移除然後繼續壓下一行代碼塊進去執行。上面代碼塊的堆棧圖,當主線程接受了request後,程序被壓進同步執行的sleep執行塊(我們假設這裏就是程序的業務處理),如果在這10s內有第二個request進來就會被壓進stack裏面等待10s執行完成後再進一步處理下一個請求,後面的請求都會被掛起等待前面的同步執行完成後再執行。

那麼我們會疑問:爲什麼一個單線程的效率可以這麼高,同時處理數萬級的併發而不會造成阻塞呢?就是我們下面所說的--------事件驅動。

事件驅動/事件循環
Event Loop is a programming construct that waits for and dispatches events or messages in a program.
在這裏插入圖片描述
1、每個Node.js進程只有一個主線程在執行程序代碼,形成一個執行棧(execution context stack)。
2、主線程之外,還維護了一個"事件隊列"(Event queue)。當用戶的網絡請求或者其它的異步操作到來時,node都會把它放到Event Queue之中,此時並不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
3、主線程代碼執行完畢完成後,然後通過Event Loop,也就是事件循環機制,開始到Event Queue的開頭取出第一個事件,從線程池中分配一個線程去執行這個事件,接下來繼續取出第二個事件,再從線程池中分配一個線程去執行,然後第三個,第四個。主線程不斷的檢查事件隊列中是否有未執行的事件,直到事件隊列中所有事件都執行完了,此後每當有新的事件加入到事件隊列中,都會通知主線程按順序取出交EventLoop處理。當有事件執行完畢後,會通知主線程,主線程執行回調,線程歸還給線程池。
4、主線程不斷重複上面的第三步。

總結:

我們所看到的node.js單線程只是一個js主線程,本質上的異步操作還是由線程池完成的,node將所有的阻塞操作都交給了內部的線程池去實現,本身只負責不斷的往返調度,並沒有進行真正的I/O操作,從而實現異步非阻塞I/O,這便是node單線程和事件驅動的精髓之處了。

Node.js 中的事件循環的實現:
Node.js採用V8作爲js的解析引擎,而I/O處理方面使用了自己設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不同操作系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現。 在src/node.cc中:

Environment* CreateEnvironment(IsolateData* isolate_data,
                               Local<Context> context,
                               int argc,
                               const char* const* argv,
                               int exec_argc,
                               const char* const* exec_argv) {
  Isolate* isolate = context->GetIsolate();
  HandleScope handle_scope(isolate);
  Context::Scope context_scope(context);
  auto env = new Environment(isolate_data, context,
                             v8_platform.GetTracingAgent());
  env->Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
  return env;
}

這段代碼建立了一個node執行環境,可以看到第三行的uv_default_loop(),這是libuv庫中的一個函數,它會初始化uv庫本身以及其中的default_loop_struct,並返回一個指向它的指針default_loop_ptr。 之後,Node會載入執行環境並完成一些設置操作,然後啓動event loop:

{
    SealHandleScope seal(isolate);
    bool more;
    env.performance_state()->Mark(
        node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks(isolate);

      more = uv_loop_alive(env.event_loop());
      if (more)
        continue;

      RunBeforeExit(&env);

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
    } while (more == true);
    env.performance_state()->Mark(
        node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
  }

  env.set_trace_sync_io(false);

  const int exit_code = EmitExit(&env);
  RunAtExit(&env);

more用來標識是否進行下一輪循環。 env->event_loop()會返回之前保存在env中的default_loop_ptr,uv_run函數將以指定的UV_RUN_DEFAULT模式啓動libuv的event loop。如果當前沒有I/O事件也沒有定時器事件,則uv_loop_alive返回false。

在這裏插入圖片描述
timers 階段:這個階段執行timer(setTimeout、setInterval)的回調
I/O callbacks 階段:執行一些系統調用錯誤,比如網絡通信的錯誤回調
idle, prepare 階段:僅node內部使用
poll 階段:獲取新的I/O事件, 適當的條件下node將阻塞在這裏
check 階段:執行setImmediate()的回調
close callbacks 階段:執行socket的close事件回調。
核心函數uv_run:源碼 核心源碼

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

//首先檢查我們的loop還是否活着
//活着的意思代表loop中是否有異步任務
//如果沒有直接就結束
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);
//傳說中的事件循環,你沒看錯了啊!就是一個大while
  while (r != 0 && loop->stop_flag == 0) {
 //更新事件階段
    uv__update_time(loop);

 //處理timer回調
    uv__run_timers(loop);

 //處理異步任務回調 
    ran_pending = uv__run_pending(loop);

//沒什麼用的階段
    uv__run_idle(loop);
    uv__run_prepare(loop);

    //這裏值得注意了
    //從這裏到後面的uv__io_poll都是非常的不好懂的
    //先記住timeout是一個時間
    //uv_backend_timeout計算完畢後,傳遞給uv__io_poll
    //如果timeout = 0,則uv__io_poll會直接跳過
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);

    //就是跑setImmediate
    uv__run_check(loop);

    //關閉文件描述符等操作
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

代碼中我已經寫得很詳細了,相信不熟悉c代碼的各位也能輕易搞懂,沒錯,事件循環就是一個大while而已!神祕的面紗就此揭開。

uv__io_poll階段
這個階段設計得非常巧妙,這個函數第二個參數是一個timeout參數,而這個timeOut由來自uv_backend_timeout函數,我們進去一探究竟!

源碼

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;

  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;

  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;

  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}

原來是一個多步if函數,我們一個一個分析

  1. stop_flag:這個標記是 0的時候,意味着事件循環跑完這一輪就退出了,返回的時間是0

  2. !uv__has_active_handles和!uv__has_active_reqs:看名字都知道,如果沒有任何的異步任務(包括timer和異步I/O),那timeOut時間一定就是0了

  3. QUEUE_EMPTY(idle_handles)和QUEUE_EMPTY(pending_queue):異步任務是通過註冊的方式放進了pending_queue中,無論是否成功,都已經被註冊,如果什麼都沒有,這兩個隊列就是空,所以沒必要等了。

  4. closing_handles:我們的循環進入了關閉階段,沒必要等待了

以上所有條件判斷來判斷去,爲的就是等這句話return uv__next_timeout(loop);這句話,告訴了uv__io_poll說:你到底停多久,接下來,我們繼續看這個神奇的uv__next_timeout是怎麼獲取時間的。

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  heap_node = heap_min((const struct heap*) &loop->timer_heap);
  if (heap_node == NULL)
    return -1; /* block indefinitely */

  handle = container_of(heap_node, uv_timer_t, heap_node);
  if (handle->timeout <= loop->time)
    return 0;

//這句代碼給出了關鍵性的指導
  diff = handle->timeout - loop->time;

//不能大於最大的INT_MAX
  if (diff > INT_MAX)
    diff = INT_MAX;

  return diff;
}

等待結束以後,就會進入check 階段.然後進入closing_handles階段,至此一個事件循環結束。

總結:
1、Nodejs與操作系統交互,我們在 Javascript中調用的方法,最終都會通過 process.binding 傳遞到 C/C++ 層面,最終由他們來執行真正的操作。Node.js 即這樣與操作系統進行互動。

2、nodejs所謂的單線程,只是主線程是單線程,所有的網絡請求或者異步任務都交給了內部的線程池去實現,本身只負責不斷的往返調度,由事件循環不斷驅動事件執行。

3、Nodejs之所以單線程可以處理高併發的原因,得益於libuv層的事件循環機制,和底層線程池實現。

4、Event loop就是主線程從主線程的事件隊列裏面不停循環的讀取事件,驅動了所有的異步回調函數的執行,Event loop總共7個階段,每個階段都有一個任務隊列,當所有階段被順序執行一次後,event loop 完成了一個 tick。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章