美文网首页
Node异步

Node异步

作者: 林帅并不帅 | 来源:发表于2017-12-12 16:44 被阅读0次

    理解闭包

    从形式来看,闭包就是在函数里面定义一个函数,从特点来说,子函数能够读写父函数的局部变量

    function parent() {
       var count = 0;
       return function children(){
          count++;
          console.log(count);
       }
    }
    
    var children = parent();
    children();  // 1
    children();  // 2
    

    闭包能够访问外部函数的变量,在外部函数执行完毕后,外部函数中的变量内存依然存在并未释放,它的生命周期会保存到children变量内存被回收为止。要避免内存泄漏,就要考虑何时注销闭包函数的引用,理解它的生命周期,才能尽量避免可能产生的内存泄漏。

    所以要关注包含大对象的闭包函数对象,是否被引用到了root对象上,是否被注册到事件循环中,是否对应执行了反注册方法,是否置空,具体内存接下来再花一篇来重点分析一下。

    理解异步

    我们在接触学习node时总会听到node的单线程模型,其实这里会导致对 Node.js的单线程会有个很深的误会。事实上,这里的单线程指的是我们(开发者)编写的代码只能运行在一个线程当中(习惯称之为主线程),Node.js并没有给 Javascript 执行时创建新线程的能力,所以称为单线程,也就是所谓的主线程。 其实,Nodejs中许多异步方法在具体的实现时(NodeJs底层封装了Libuv,它提供了线程池、事件池、异步I/O等模块功能,其完成了异步方法的具体实现),内部均采用了多线程机制。

    image.png

    这里,主线程就是nodejs所谓的单线程,也就是用户javascript代码运行的线程,I/O线程即执行异步操作的线程。

    image.png

    执行node app.js的流程如上图所示:

    1)node启动,进入main函数;

    2)初始化核心数据结构 default_loop_struct;这个数据结构是事件循环的核心,当node执行到“加载js文件”时,如果用户的javascript代码中具有异步IO操作时,如读写文件。这时候,javascript代码调用–>lib模块–>C++模块–>libuv接口–>最终系统底层的API,系统返回一个文件描述符fd 和javascript代码传进来的回调函数callback,然后封装成一个IO观察者(一个uv__io_s类型的对象),保存到default_loop_struct。

    3)加载用户javascript文件,调用V8引擎接口,解析并执行javascript代码。如果有异步IO,则通过一系列调用系统底层API。

    若是网络IO,如http.get() 或者 app.listen() ,则把系统调用后返回的结果(文件描述符fd)和事件绑定的回调函数callback,一起封装成一个IO观察者,保存到default_loop_struct。

    如果是文件IO,例如在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关心的回调函数则被设置在这个对象的oncomplete_sym属性上:req_wrap->object_->Set(oncomplete_sym, callback)。对象包装完毕后,在Windows下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行。。

    至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否会阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到到了异步的目的。

    等异步线程操作完毕,通知事件循环有异步io结束,需要调用回调函数。

    4)进入事件循环,即调用libuv的事件循环入口函数uv_run();当处理完 js代码,如果有io操作,那么这时default_loop_struct是保存着对应的io观察者的。处理完js代码,main函数继续往下调用libuv的事件循环入口uv_run(),node进程进入事件循环:

    uv_run()的while循环做的就是一件事,判断default_loop_struct是否有存活的io观察者。 a. 如果没有io观察者,那么uv_run()退出,node进程退出。 b. 而如果有io观察者,那么uv_run()进入epoll_wait(),线程挂起等待,监听对应的io观察者是否有数据到来。有数据到来调用io观察者里保存着的callback(js代码),没有数据到来时一直在epoll_wait()进行等待。

    异步调用各线程流程图及关系如下:

    image.png

    理解事件循环

    事件循环的职责,就是不断得等待事件的发生,然后将这个事件的所有处理器,以它们订阅这个事件的时间顺序,依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。

    Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,上文提到的事件循环机制是它里面的实现,代码如下:

    int uv_run(uv_loop_t* loop, uv_run_mode mode) {
      int timeout;
      int r;
      int ran_pending;
    
      r = uv__loop_alive(loop);
      if (!r)
        uv__update_time(loop);
    
      while (r != 0 && loop->stop_flag == 0) {
        uv__update_time(loop);
        // timers阶段
        uv__run_timers(loop);
        // I/O callbacks阶段
        ran_pending = uv__run_pending(loop);
        // idle阶段
        uv__run_idle(loop);
        // prepare阶段
        uv__run_prepare(loop);
    
        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
          timeout = uv_backend_timeout(loop);
        // poll阶段
        uv__io_poll(loop, timeout);
        // check阶段
        uv__run_check(loop);
        // close callbacks阶段
        uv__run_closing_handles(loop);
    
        if (mode == UV_RUN_ONCE) {
          uv__update_time(loop);
          uv__run_timers(loop);
        }
    
        r = uv__loop_alive(loop);
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
      }
    
      if (loop->stop_flag != 0)
        loop->stop_flag = 0;
    
      return r;
    }
    

    每次事件循环都包含了6个阶段,对应上段代码 libuv 源码中的实现。

    image.png
    • timers 阶段

      timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。

    • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调。

    • idle, prepare 阶段:仅node内部使用。

    • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里。

      主要有2个功能:

      • 处理 poll 队列的事件
      • 当有已超时的 timer,执行它的回调函数

      在timers阶段产生的超时回调,在这个阶段会执行,直到超时timers队列为空或执行的回调达到系统上限(上限具体多少未详)。接下来even loop会去检查有无预设的setImmediate(),分两种情况:若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列。若没有预设的setImmediate(),event loop将阻塞在该阶段等待。

      这种阻塞状态会被两种情况打破,一个是timeout达到,一个是setImmediate方法执行,这时候会进入下一次loop循环,重新检查是否有超时的timers需要处理,进入下一个消息循环。

    • check 阶段:执行 setImmediate() 的回调。

    • close callbacks 阶段:执行 socketclose 事件回调

    所以为什么

    const fs = require('fs')
    
    fs.readFile('test.txt', () => {
      console.log('readFile')
      setTimeout(() => {
        console.log('timeout')
      }, 0)
      setImmediate(() => {
        console.log('immediate')
      })
    })
    

    的执行结果是

    readFile
    immediate
    timeout
    

    因为setImmediate方法打破阻塞状态优先执行check方法,而后才从超时队列中取出超时timer回调执行,再次进入阻塞状态。

    注意上文中提到setTimeout并不是严格按照时间节点来,如果在回调中执行耗时的操作,导致下次消息循环触发时间会整体延后,比如

    var sleep = require('sleep');
    setTimeout(() => {
        console.log('timeout')
    }, 100);
    setImmediate(() => {
        console.log('immediate')
        sleep.sleep(2);
    })
    

    则timeout的打印时间为2100秒以后,所以尽量不要在主线程中执行耗时操作,耗时操作尽量都放在Worker线程中。

    相关文章

      网友评论

          本文标题:Node异步

          本文链接:https://www.haomeiwen.com/subject/mlynixtx.html