Node.js之異步那些事

nodejs

Node.js? is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

Node.js官網上的介紹,其中事件驅動非阻塞I/O模型是被大家所津津樂道的,但是有多少人真正了解其究竟呢?有人可能會想到libuv,沒錯,libuv確實是其幕后英雄。那么問題又來了,到底是怎么用libuv實現(xiàn)的呢?下面我們來一探究竟。

libuv

libuv當初主要就是為Node.js開發(fā)的,提供跨平臺的事件驅動異步I/O能力,當然現(xiàn)在肯定不僅限于Node.js使用。我們先來看一下libuv的Design overview

architecture

從架構圖上看,libuv是對多個平臺上的事件驅動異步I/O庫進行了封裝,如Linux下的epoll、FreeBSD下的kqueue、Solaris下的event ports、Windows下的IOCP。

loop_iteration

上圖所描述的事件循環(huán)是libuv中最重要的概念,其中的Poll for I/O就是事件驅動異步I/O能力的核心。到這里我們有必要先了解一些基礎知識,Linux IO模式及 select、poll、epoll詳解,否則后面的東西就不是特別好理解了。

正題


經過前面的學習,應該對libuv有了一個整體的印象,總結一下, libuv其實就是把各種handleio_watcher放到事件循環(huán)里,然后每一次循環(huán)都去檢查一下是否有他們關心的事件需要處理,有則調用相應的callback,沒有則繼續(xù)循環(huán)。要想弄清楚Node.js之異步那些事,我們需要關心的是,Node.js如何運行事件循環(huán),何時把handleio_watcher放入事件循環(huán),以及如何調用相應的callback。

開始之前,本次分析的代碼版本為Node.js v0.12.6,Linux平臺。

Run

node.ccStart方法運行事件循環(huán),精華部分如下。唯一有些特別的地方就是,在一個while循環(huán)中包了兩個uv_run,模式分別是UV_RUN_ONCEUV_RUN_NOWAIT,其原因在中間的兩行注釋中已經說得很明白了。

...
    bool more;
    do {
      more = uv_run(env->event_loop(), UV_RUN_ONCE);
      if (more == false) {
        EmitBeforeExit(env);

        // Emit `beforeExit` if the loop became alive either after emitting
        // event, or after running some callbacks.
        more = uv_loop_alive(env->event_loop());
        if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
          more = true;
      }
    } while (more == true);
...

然后我們可以看看core.cuv_run方法的代碼,跟上面事件循環(huán)的流程圖是可以一一對應的。

Data Structure

繼續(xù)看代碼之前,有必要先了解一下重要的數(shù)據(jù)結構和相互的關系,以便更好的理解。

Data Structure

io_watcher

接著我之前文章Node.js之HelloWorld背后的大坑的思路,還拿Hello World舉例子,跟libuv有關的代碼都在tcp_warp.cc里面了。

  • TCPWrap::New
New

stream.cuv__stream_init方法有如下代碼,將io_watchercb設置為uv__stream_io,fd設置為-1,這里只是在stream層面做的初始化設置,后面到tcp層面還會有相應的改變。

  uv__io_init(&stream->io_watcher, uv__stream_io, -1);
  • TCPWrap::Bind
Bind

tcp.cmaybe_new_socket方法中,uv__socket方法生成了新的fduv__stream_open方法將其設置到io_watcherfd。

  • TCPWrap::Listen
Listen

tcp.cuv_tcp_listen方法中有如下代碼,將io_watchercb設置為uv__server_iouv__server_io里面會調用connection_cb,connection_cb已經被設置為cb,而這個cb正是tcp_wrap.cc中的TCPWrap::OnConnection方法。

...
  tcp->connection_cb = cb;

  /* Start listening for connections. */
  tcp->io_watcher.cb = uv__server_io;
  uv__io_start(tcp->loop, &tcp->io_watcher, UV__POLLIN);
...

core.cuv__io_start方法有如下代碼,利用void* watcher_queue[2]變量將io_watcher加入到uv_loop_t的隊列中去,具體操作詳見queue.h。將uv_loop_tuv__io_t** watchers當做數(shù)組使用,fd為下標,io_watcher為對應的值。

...

  if (QUEUE_EMPTY(&w->watcher_queue))
    QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);

  if (loop->watchers[w->fd] == NULL) {
    loop->watchers[w->fd] = w;
    loop->nfds++;
  }
...

uv__io_poll

linux-core.c中的uv__io_poll方法,一行一行的讀就可以了,前面的鋪墊已經做得很充分了,只要讀懂謎底便可揭曉。

未完


  • 接下來我們來說說process.nextTick(callback)的事,在node.js中定義如下,把callback放到了nextTickQueue隊列中,那么Node.js是在什么時候消費這個隊列的呢?
    function nextTick(callback) {
      // on the way out, don't bother. it won't get fired anyway.
      if (process._exiting)
        return;

      var obj = {
        callback: callback,
        domain: process.domain || null
      };

      nextTickQueue.push(obj);
      tickInfo[kLength]++;
    }
  • tcp_wrap.ccTCPWrap::OnConnection方法有如下代碼,MakeCallback方法的出處如下圖。
  tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv);
MakeCallback
  • async-wrap.ccMakeCallback方法有如下代碼。
  env()->tick_callback_function()->Call(process, 0, NULL);
  • node.ccSetupNextTick方法有如下代碼,對tick_callback_function()進行了設定。
  env->set_tick_callback_function(args[1].As<Function>());
  • node.ccSetupProcessObject方法有如下代碼,SetupNextTick被設定為process中的_setupNextTick方法。
  NODE_SET_METHOD(process, "_setupNextTick", SetupNextTick);
  • node.jsstartup.processNextTick方法有如下代碼。
  process._setupNextTick(tickInfo, _tickCallback, _runMicrotasks);
  • node.js_tickCallback方法代碼如下,消費nextTickQueue隊列中的callback方法。
    function _tickCallback() {
      var callback, threw, tock;

      scheduleMicrotasks();

      while (tickInfo[kIndex] < tickInfo[kLength]) {
        tock = nextTickQueue[tickInfo[kIndex]++];
        callback = tock.callback;
        threw = true;
        try {
          callback();
          threw = false;
        } finally {
          if (threw)
            tickDone();
        }
        if (1e4 < tickInfo[kIndex])
          tickDone();
      }

      tickDone();
    }

省略去中間步驟,實際上是產生了如下的調用關系。

TCPWrap::OnConnection()
↓↓↓
_tickCallback()

總結


簡單說,整個過程是這樣的,事件循環(huán)中有相應I/O事件發(fā)生的時候,libuv調用Node.js C++部分的回調,C++部分調用JavaScript部分的回調,順便調用nextTick設定的回調。

還是認真讀代碼吧,以上寫的僅供參考。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容