美文网首页
异步I/O—事件循环机制

异步I/O—事件循环机制

作者: 励志摆脱懒癌的少女酱 | 来源:发表于2018-07-15 14:14 被阅读17次

    I/O简介

    1.I/O操作:内核在进行文件I/O操作时,通过文件描述符(fd:一个整数—应用程序和内核之间的凭证)进行管理,应用程序若需要进行I/O调用时,先打开文件描述符,然后根据其去实现文件的数据读写。

    以进程的读入为例:

    * 第一步:数据会先被拷贝到操作系统内核的缓冲区:等待所有数据都准备好或者一直在等待数据,有数据的时候将数据拷贝到系统内核;

    *第二步:将内核缓存中数据拷贝到用户进程缓冲区中;

    2.IO模型:同步IO和异步IO

    同步IO模型:第二步是由用户进程来完成,在进行复制的过程中不能做其他事情

        *  阻塞I/O:在内核请求IO设备响应指令发出后,进程挂起,内核等待外部IO响应;IO完成传送数据到kernel buffer,数据再从buffer复制到用户的进程空间,默认情况下所有套接字都是阻塞的。

        * 非阻塞I/O:在内核请求IO设备响应指令发出后,数据就开始准备,在此期间用户进程没有阻塞,也就是没有挂起,通过轮询的方式来询问或者check数据有没有传送到kernel buffer中;但是第二个阶段(数据从kernel buffer复制到用户进程空间)依然是阻塞的。

        *I/O多路复用

        *信号驱动I/O

    异步IO模型将数据复制到用户缓冲区这一过程由内核完成,完成后通知用户该缓冲区可用了即可,用户在内核复制的过程中可以进行其他的操作;

        *异步I/O:告知内核启动某个操作,并让内核在整个操作(包括把数据从内核复制到用户缓冲区这一过程)完成后再通知我们。

    以Ajax请求为例

    同步阻塞IO的Ajax:

        * 主线程:你好,Ajax线程,请你帮我发个http请求吧,我把请求地址和参数发给你;

        * Ajax线程:好的,你等着哈;

        * 一段时间后,Ajax线程完成任务,将响应结果返回给主线程,主线程拿到了后继续执行后面的任务。

    同步非阻塞IO的Ajax:

        * 主线程:你好,Ajax线程,请你帮我发个http请求吧,我把请求地址和参数发给你;

        * Ajax线程:我去干,你等着;

        * 主线程:Ajax线程,你干完了吗?

        * Ajax线程:没有,等着;

        * 就这样轮询,知道Ajax线程得到响应后,就响应结果返回给主线程,主线程再执行后面的任务。

    异步Ajax:

        * 主线程:你好,Ajax线程,请帮我发个http请求吧,我把请求地址和参数都给你;

        * Ajax线程:ok,我马上去发,你先去忙点别的吧;

        * 主线程:ok,拿到响应call me(回调函数);

        * 然后,主线程去做其他的事情了,Ajax线程完成任务后,通过回调函数通知主线程响应到达了。

    js是单线程语言(js执行在单线程中,而内部I/O任务其实是另有线程池来完成的),对I/O的控制是通过异步实现的—事件循环机制;

    同步任务与异步任务

    (1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

    (2)异步任务:不进入主线程而是进入任务队列的任务,只有等主线程的任务执行完毕后,任务队列开始通知主线程,请求执行任务,该任务进入主线程执行;

    事件循环(Event Loop)

           Javascript是单线程的,但是却能执行异步任务,这主要是因为 JS 中存在事件循环和任务队列。

    (1)事件循环(Event Loop):实现异步的一种机制,以任务为单位;

    (2)任务队列(Task Queue):事件(消息)队列—只要异步任务有了运行结果,就在任务队列中放置一个事件;

         主线程读取"任务队列",就是读取里面有哪些事件,执行其对应的回调函数(被主线程挂起的代码),因此异步任务必须指定回调函数,当异步任务完成后,通过回调函数通知主线程。

    浏览器中的事件循环模型

    1.线程、事件循环和任务队列

         浏览器对不同的异步操作,将其添加到任务队列的时机也不同—由浏览器内核的webcore来执行,其包含3种webAPI:

    * DOM Binding:处理DOM绑定事件,若绑定事件触发时,回调函数立即被webcore添加到任务队列中;

    * network:处理ajax请求,在网络请求返回时,才将对应的回调函数添加到队列中;

    * timer:对setTimeout等计时器进行延时处理,当时间到达时才会将回调函数添加到任务队列中;

    (3)主线程:执行同步代码,只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。

    2.不同任务队列的优先级

    (1)macrotask(宏任务:task):script(整体代码) > setTimeout > setInterval > setImmediate > I/O > UI rending;js引擎一次性只能取出一个宏任务(对应一个待处理的事件);

    (2)microtask(微任务):process.nextTick > Promises;执行完一个task的标识是microtask为空;

    Node中的事件循环模型

          Node的异步语法比浏览器更复杂,因为他可以和内核对话,因此出现了libuv异步IO库,负责各种回调函数的执行时间,使异步任务排队依次回到主线程执行。

          与浏览器大致相同,只是Node中事件循环分不同的阶段

    1.事件循环执行顺序

    每一个盒子都是一个阶段,而每个阶段都有一个FIFO的回调队列要执行,当队列被执行完或执行的回调函数数量达到上限后,事件循环才会进入下一个阶段;

    异步任务分两种:

    (1)追加在本轮循环的异步任务;

    (2)追加到次轮循环的异步任务;

    2.阶段详情

    (1)timers(定时器阶段):处理setTimeout()和setInterval()设定的回调函数队列

           一个timer事件指定一个下限时间而不是准确的时间,在达到这个下限时间后+主线程空闲时,执行该事件对应的回调函数,从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers(poll阶段会控制是否进入下个timers阶段)

    (2)I/O callbacks阶段:执行一些系统操作的回调;

    (3)idle、prepare:仅供libuv内部调用;

    (4)poll(轮询阶段): 等待还未返回的I/O事件,任何异步方法(除timers、setImmediate、close外)完成时,都会将其加到poll queue里,并立即执行

    * 主要功能:

            (i) 处理poll队列里的事件;

            (ii)执行下限时间已经达到的timers的回调(进入下一个事件循环) ;

    *当事件循环进入poll阶段:

            (i)poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。

            (ii)poll队列为空的时候,这里有两种情况。

                    1)如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。

                    2)如果代码没有被设定setImmediate()设定回调

                           —> 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列(进入下一个事件循环阶段了)

                           —> 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。

    (5)check阶段:执行setImmediate()设定的回调;

    * setImmediate()实际上是一个特殊的timer,跑在事件循环中的一个独立的阶段;它使用libuv的API来设定在(i)poll阶段结束后立即执行回调;(ii)poll阶段空闲时,不让阻塞在poll阶段直接跳到check阶段执行回调。

    (6)close callbacks阶段:如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

    3.setTimeout和setImmediate的执行顺序

            setTimeout、setInterval、setImmediate的回调函数,都是追加到次轮循环

    先看两个例子:

    :下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1

    其结果的顺序是随机输出的

    分析:

    (1)setImmediate() > setTimeout()

         机器性能很好,当进入timers阶段,setTimeout的下限时间没到(即1ms之内就进入timers阶段了),事件循环到了poll阶段,其队列为空,此时代码被setImmediate(),则不阻塞在poll阶段也不进入下一个事件循环,而是直接进入check阶段执行setImmediate()的回调;

    (2)setTimeout() > setImmediate()

         机器性能一般,当进入timers阶段时,1ms已经过去了,(setTimeout(fn, 0) 等价于setTimeout(fn, 1)),setTimeout()的回调会首先执行,然后才执行setImmediate()队列;

    这种情况下,setImmediate始终优先于setTimeout

    分析:

    因为在timers阶段执行外部的setTimeout()回调后,内层的setTimeout()进入timers阶段的队列和setImmediate()进入check阶段的队列,进入poll阶段发现poll阶段的队列为空,此时代码被setImmediate(),直接进入check阶段执行回调,之后在第二个事件循环的timers阶段再去执行相应的回调。

    综上:

    * 如果两者都在主模块中调用,那么执行先后取决于进程性能,也就是随机。

    * 如果两者都不在主模块调用(被一个异步操作包裹),那么setImmediate的回调永远先执行。

    4. process.nextTick()和Promise()的执行顺序

         我们可以将它们理解为一个微任务,其实不属于事件循环的一部分,它们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行,即回调函数追加在本轮循环,即同步任务一旦执行完成就开始执行它们;

    * process.nextTick()会把回调塞入nextTickQueue,nextTickQueue将在当前操作完成后处理,不管目前处于event loop的哪个阶段,即立即在本阶段执行回调。

    结果:timeout1、promise1、promise2、setTimeout2

    相关文章

      网友评论

          本文标题:异步I/O—事件循环机制

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