美文网首页
前端进阶全栈-Node的异步IO

前端进阶全栈-Node的异步IO

作者: stevekeol | 来源:发表于2020-01-11 18:56 被阅读0次

    本文力图详尽解释node的异步IO:

    • 异步IO的产生背景
    • Node中的异步IO具体的实现
    • 非I/O的异步API

    一.为什么要异步I/O?

    Node面向网络而设计,Web应用现如今已不再是单台服务器可以胜任,在跨网络结构下,并发已是现代编程的标配。

    以下从“用户体验”和“资源分配”两方面阐述异步I/O的必要性。

    1. 用户体验
    2. 资源分配

    假若业务场景有一组互不相关的任务要完成,有两种方法:

    • 单线程串行依次完成
    • 多线程并行完成

    串行执行的缺点:性能上,任意一个稍慢的任务都会导致后续执行代码被阻塞。本可以并行利用CPU等资源,但同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。

    多线程并行的缺点: (1)创建线程和执行期线程上下文切换的开销较大;(2)多线程编程经常面临锁,状态同步等问题。

    Node给出的方案:
    利用单线程,远离多线程死锁,状态同步等问题;利用异步I/O让单线程原理阻塞,以更好的利用CPU等硬件资源。

    二. Node底层是如何实现异步I/O的?

    记住四个关键词:

    • 事件循环
    • 观察者
    • 请求对象
    • 线程池

    先简洁直白的大致描述一下:

    Node发出一个异步调用请求,然后继续执行业务代码;该请求被封装成一个"请求对象"并放入系统的线程池;待有可用线程完成该请求后,将异步结果放入该请求对象,并通知IOCP;每一次"事件循环"中,"观察者"尝试从IOCP中取出可用的"请求对象"并放入事件队列中, 并取出该对象中的回调函数和结果作为一个事件调用执行。

    1. 事件循环

    Node自身的执行模型----事件循环。

    在进程启动时,Node便会创建一个类似while(true)的循环,每执行一次循环体的过程称为Tick。每个 Tick的过程就是查看是否有事件待处理,如有,就取出事件并执行这些相关的回调函数。然后进入下一个循环。如果不再有事件处理,就退出进程。

    2. 观察者

    正如上文提到,如何判断每个Tick过程中,是否有事件需要处理呢?这就引出了”观察者“。

    事件循环是一个典型的“生产者/消费者”模型。异步I/O,网络请求等则是事件的生产者,源源不断的为Node提供不同类型的事件,这些事件被传递到对应的观察者那里。事件循环则从观察者那里取出事件并处理。

    在windows下,这个循环基于IOCP创建,而在*nix下则基于多进程创建。

    3. 请求对象
    • 以fs.open()为例,javascript调用node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用。
    • 系统调用中,对应的是uv_fs_open()方法,该方法创建了一个FSReqWrap请求对象。该对象封装了从javascript层传入的参数,当前方法和回调函数等所有的状态。
    • 系统层将该请求对象放入线程池等待执行。
    • (此时,javascript调用立即返回,即javascript层发起的异步调用的第一阶段到此结束,javascript线程可以继续执行当前任务的后续操作)
    4. 执行回调

    以上提到,异步I/O的第一阶段:组装请求对象,放入线程池等待执行;
    第二阶段:回调通知。

    • 线程池中的I/O操作完成后,会将结果挂载在请求对象上,然后通过PostQuenedCopletedStatus()通知系统层的IOCP,告知当前的操作对象已经操作完成。

    • Node层在每一次Tick的执行中,会调用IOCP的GetQuenedCompletedStatus()方法检查是否有执行完的请求,如果存在则将该请求对象加入事件队列中,并取出结果和回调函数调用执行。

    从以上可看出:Windows下主要是通过IOCP来向系统内核发送已完成的I/O调用和从系统内核中取出已完成的I/O操作,配以事件循环,以此完成异步I/O的操作。

    三. 非I/O的异步API

    • setTimeout()
    • setInterval()
    • setImmediate()
    • process.nextTick()
    1. 定时器

    调用setTimeout()或setInterval()创建的定时器会被插入到定时器内部的一个红黑树上。每次Tick执行时,会从该红黑树上迭代取出定时器对象,检查是否超过规定时间,如果超过就形成一个事件,并立即执行其回调函数。

    2. process.nextTick()

    每次调用process.nextTick()会将回调函数放入一个队列数组中,下一次Tick时取出该数组中全部的回调函数并执行。(时间负责度O(1))

    3. setImmediate()

    setImmediate()类似于process.nextTick(),都是将回调函数延迟执行。

    区别在于:

    • 事件循环对于观察者是有先后顺序的。setImmediate()对应的观察者的优先级低于process.nextTick()对应的观察者。
    • process.nextTick()的回调函数保存在数组中,setImmediate()的回调函数保存在链表中。行为上,processTick()在每轮循环中会将数组中的回调函数全部取出并执行,而setImmediate()在每轮循环中执行链表中的一个回到函数。

    佐证代码:

    process.nextTick(funciton() {
      console.log('nextTick延迟执行1');
    })
    
    process.nextTick(funciton() {
      console.log('nextTick延迟执行2');
    })
    
    setImmediate(funciton() {
      console.log('setImmediate延迟执行1');
      process.nextTick(function() {
        console.log('强势插入');
      })
    })
    
    setImmediate(fucntion() {
      console.log('setImmediate延迟执行2');
    })
    
    console.log('正常执行');
    
    //正常执行
    //nextTick延迟执行1
    //nextTick延迟执行2
    //setImmediate延迟执行1
    //强势插入
    //setImmediate延迟执行2
    

    四. 总结

    事件驱动的本质:通过主循环加事件触发的方式来运行程序。


    注:以上均是自己技术栈的整理,仅供备忘。如需交流:stevekeol(微信号)

    相关文章

      网友评论

          本文标题:前端进阶全栈-Node的异步IO

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