nodejs 异步I/O和事件驱动

KCCTravis 5年前
   <h2>nodejs 异步I/O和事件驱动</h2>    <p>注:本文是对众多博客的学习和总结,可能存在理解错误。请带着怀疑的眼光,同时如果有错误希望能指出。</p>    <p>接触 nodejs 有两个月,对 nodejs 的两大特性一直有点模糊,即 异步IO 和 事件驱动 。通过对 <strong> <em>《深入浅出nodejs》</em> </strong> 和几篇博客的阅读以后,有了大致的了解,总结一下。</p>    <h2>几个例子</h2>    <p>在开始之前,先来看几个简单例子,这也是我在使用 nodejs 时候遇到的几个比较困惑的例子。</p>    <h3>example 1</h3>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example1');    debug("begin");    setTimeout(function(){      debug("timeout1");  });    setTimeout(function(){      debug("timeout2");  });    debug('end');  /** 运行结果  Sat, 21 May 2016 08:41:09 GMT example1 begin  Sat, 21 May 2016 08:41:09 GMT example1 end  Sat, 21 May 2016 08:41:09 GMT example1 timeout1  Sat, 21 May 2016 08:41:09 GMT example1 timeout2  */</code></pre>    <p>question 1</p>    <p>为何 timeout1 和 timeout2 的结果会在 end 后面?</p>    <h3>example 2</h3>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example2');    debug("begin");    setTimeout(function(){      debug("timeout1");  });    setTimeout(function(){      debug("timeout2");  });    debug('end');    while(true);  /**  运行结果  Sat, 21 May 2016 08:45:47 GMT example2 begin  Sat, 21 May 2016 08:45:47 GMT example2 end  */</code></pre>    <p>question 2</p>    <p>为何 timeout1 和 timeout2 没有输出到终端? while(true) 到底阻塞了什么?</p>    <h3>example 3</h3>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example3');    debug("begin");    setTimeout(function(){      debug("timeout1");      while (true);  });    setTimeout(function(){      debug("timeout2");  });    debug('end');  /**  运行结果  Sat, 21 May 2016 08:49:12 GMT example3 begin  Sat, 21 May 2016 08:49:12 GMT example3 end  Sat, 21 May 2016 08:49:12 GMT example3 timeout1  */</code></pre>    <p>question 3</p>    <p>为什么 timeout1 中回调函数会阻塞 timeout2 中的回调函数的执行?</p>    <h3>example 4</h3>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example4');    debug("begin");    setTimeout(function(){      debug("timeout1");      /**       * 模拟计算密集       */      for(var i = 0 ; i < 1000000 ; ++i){          for(var j = 0 ; j < 100000 ; ++j);      }  });    setTimeout(function(){      debug("timeout2");  });    debug('end');  /**  Sat, 21 May 2016 08:53:27 GMT example4 begin  Sat, 21 May 2016 08:53:27 GMT example4 end  Sat, 21 May 2016 08:53:27 GMT example4 timeout1  Sat, 21 May 2016 08:54:09 GMT example4 timeout2  //注意这里的时间晚了好久  */</code></pre>    <p>question 4</p>    <p>和上面的问题一样,为何 timeout1 的计算密集型工作将会阻塞 timeout2 的回调函数的执行?</p>    <h3>example 5</h3>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example5');    debug("begin");    fs.readFile('package.json','utf-8',function(err,data){      if(err)            debug(err);      else          debug("get file content");  });    setTimeout(function(){      debug("timeout2");  });    debug('end');  /** 运行结果  Sat, 21 May 2016 08:59:14 GMT example5 begin  Sat, 21 May 2016 08:59:14 GMT example5 end  Sat, 21 May 2016 08:59:14 GMT example5 timeout2  Sat, 21 May 2016 08:59:14 GMT example5 get file content  */</code></pre>    <p>question 5</p>    <p>为何读取文件的 IO 操作不会阻塞 timeout2 的执行?</p>    <p>接下来我们就带着上面几个疑惑去理解 nodejs 中的 异步IO 和 事件驱动 是如何工作的。</p>    <h2>异步IO(asynchronous I/O)</h2>    <p>首先来理解几个容易混淆的概念, 阻塞IO(blocking I/O) 和 非阻塞IO(non-blocking I/O) , 同步IO(synchronous I/O)和异步IO(synchronous I/O) 。</p>    <p>博主一直天真的以为 非阻塞I/O 就是 异步I/O T_T, apue 一直没有读懂。</p>    <h3>阻塞I/O 和 非阻塞I/O</h3>    <p>简单来说, <strong>阻塞I/O</strong> 就是当用户发一个读取文件描述符的操作的时候,进程就会被阻塞,直到要读取的数据全部准备好返回给用户,这时候进程才会解除 block 的状态。</p>    <p>那 <strong>非阻塞I/O</strong> 呢,就与上面的情况相反,用户发起一个读取文件描述符操作的时,函数立即返回,不作任何等待,进程继续执行。但是程序如何知道要读取的数据已经准备好了呢?最简单的方法就是轮询。</p>    <p>除此之外,还有一种叫做 IO多路复用 的模式,就是用一个阻塞函数同时监听多个文件描述符,当其中有一个文件描述符准备好了,就马上返回,在 linux 下, select , poll , epoll 都提供了 IO多路复用 的功能。</p>    <h3>同步I/O 和 异步I/O</h3>    <p>那么 同步I/O 和 异步I/O 又有什么区别么?是不是只要做到 非阻塞IO 就可以实现 异步I/O 呢?</p>    <p>其实不然。</p>    <ul>     <li> <p>同步I/O(synchronous I/O) 做 I/O operation 的时候会将process阻塞,所以 阻塞I/O , 非阻塞I/O , IO多路复用I/O 都是 同步I/O 。</p> </li>     <li> <p>异步I/O(asynchronous I/O) 做 I/O opertaion 的时候将不会造成任何的阻塞。</p> </li>    </ul>    <p>非阻塞I/O 都不阻塞了为什么不是 异步I/O 呢?其实当 非阻塞I/O 准备好数据以后还是要阻塞住进程去内核拿数据的。所以算不上 异步I/O 。</p>    <p>这里借一张图(图来自这里)来说明他们之间的区别</p>    <p><img src="https://simg.open-open.com/show/2d7692f530bd9e6844b251332c4616b8.png"> ][1]</p>    <p>更多IO更多的详细内容可以在这里找到:</p>    <ul>     <li> <p><a href="https://segmentfault.com/a/1190000003063859?utm_source=Weibo&utm_medium=shareLink&utm_campaign=socialShare#articleHeader0" rel="nofollow,noindex">Linux IO模式及 select、poll、epoll详解</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959673487091350060" rel="nofollow,noindex">select / poll / epoll: practical difference for system architects</a></p> </li>    </ul>    <h2>事件驱动</h2>    <p>事件驱动(event-driven) 是 nodejs 中的第二大特性。何为 事件驱动 呢?简单来说,就是通过监听事件的状态变化来做出相应的操作。比如读取一个文件,文件读取完毕,或者文件读取错误,那么就触发对应的状态,然后调用对应的回掉函数来进行处理。</p>    <h3>线程驱动和事件驱动</h3>    <p>那么 线程驱动 编程和 事件驱动 编程之间的区别是什么呢?</p>    <ul>     <li> <p>线程驱动 就是当收到一个请求的时候,将会为该请求开一个新的线程来处理请求。一般存在一个线程池,线程池中有空闲的线程,会从线程池中拿取线程来进行处理,如果线程池中没有空闲的线程,新来的请求将会进入队列排队,直到线程池中空闲线程。</p> </li>     <li> <p>事件驱动 就是当进来一个新的请求的时,请求将会被压入队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。</p> </li>    </ul>    <p>对于 事件驱动 编程来说,如果某个时间的回调函数是 计算密集型 ,或者是 阻塞I/O ,那么这个回调函数将会阻塞后面所有事件回调函数的执行。这一点尤为重要。</p>    <h2>nodejs的事件驱动和异步I/O</h2>    <h3>事件驱动模型</h3>    <p>上面介绍了那么多的概念,现在我们来看看 nodejs 中的 事件驱动 和 异步I/O 是如何实现的.</p>    <p>nodejs 是 <strong>单线程(single thread)</strong> 运行的,通过一个 <strong>事件循环(event-loop)</strong> 来循环取出 <strong>消息队列(event-queue)</strong> 中的消息进行处理,处理过程基本上就是去调用该 <strong>消息</strong> 对应的回调函数。 <strong>消息队列</strong> 就是当一个事件状态发生变化时,就将一个消息压入队列中。</p>    <p>nodejs 的时间驱动模型一般要注意下面几个点:</p>    <ul>     <li> <p>因为是 <strong>单线程</strong> 的,所以当顺序执行 js 文件中的代码的时候, <strong>事件循环</strong> 是被暂停的。</p> </li>     <li> <p>当 js 文件执行完以后, <strong>事件循环</strong> 开始运行,并从 <strong>消息队列</strong> 中取出消息,开始执行回调函数</p> </li>     <li> <p>因为是 <strong>单线程</strong> 的,所以当回调函数被执行的时候, <strong>事件循环</strong> 是被暂停的</p> </li>     <li> <p>当涉及到I/O操作的时候, nodejs 会开一个独立的线程来进行 异步I/O 操作,操作结束以后将消息压入 <strong>消息队列</strong> 。</p> </li>    </ul>    <p>下面我们从一个简单的 js 文件入手,来看看 nodejs 是如何执行的。</p>    <pre>  <code class="language-javascript">var fs = require("fs");  var debug = require('debug')('example1');    debug("begin");    fs.readFile('package.json','utf-8',function(err,data){      if(err)            debug(err);      else          debug("get file content");  });    setTimeout(function(){      debug("timeout2");  });    debug('end'); // 运行到这里之前,事件循环是暂停的</code></pre>    <ol>     <li> <p>同步执行 debug("begin")</p> </li>     <li> <p>异步调用 fs.readFile() ,此时会开一个新的线程去进行 异步I/O 操作</p> </li>     <li> <p>异步调用 setTimeout() ,马上将超时信息压入到 <strong>消息队列</strong> 中</p> </li>     <li> <p>同步调用 debug("end")</p> </li>     <li> <p>开启 <strong>事件循环</strong> ,弹出 <strong>消息队列</strong> 中的信息(目前是超时信息)</p> </li>     <li> <p>然后执行信息对应的回调函数( <strong>事件循环</strong> 又被暂停)</p> </li>     <li> <p>回调函数执行结束后,开始 <strong>事件循环</strong> (目前 <strong>消息队列</strong> 中没有任何东西,文件还没读完)</p> </li>     <li> <p>异步I/O 读取文件完毕,将消息压入 <strong>消息队列(</strong> 消息中含有文件内容或者是出错信息)</p> </li>     <li> <p>事件循环取得消息,执行回调</p> </li>     <li> <p>程序退出。</p> </li>    </ol>    <p>这里借一张图来说明 nodejs 的事件驱动模型(图来自 <a href="/misc/goto?guid=4959673487174767869" rel="nofollow,noindex">这里</a> )</p>    <p><img src="https://simg.open-open.com/show/2000d79954b0bb0a859d6560bb73ae22.png"></p>    <p>][2]</p>    <p>这里最后要说的一点就是如何手动将一个函数推入队列, nodejs 为我们提供了几个比较方便的方法:</p>    <ul>     <li> <p>setTimeout()</p> </li>     <li> <p>process.nextTick()</p> </li>     <li> <p>setImmediate()</p> </li>    </ul>    <h3>异步I/O</h3>    <p>nodejs 中的 异步I/O 的操作是通过 libuv 这个库来实现的,包含了 window 和 linux 下面的 异步I/O 实现,博主也没有研究过这个库,感兴趣的读者可以移步到 <a href="/misc/goto?guid=4959644552947080670" rel="nofollow,noindex">这里</a></p>    <h3>问题答案</h3>    <p>好,到目前为止,已经可以回答上面的问题了</p>    <p>question 1</p>    <p>为何 timeout1 和 timeout2 的结果会在end后面?</p>    <p>answer 1</p>    <p>因为此时 timeout1 和 timeout2 只是被异步函数推入到了队列中, <strong>事件循环</strong> 还是暂停状态</p>    <p>question 2</p>    <p>为何 timeout1 和 timeout2 没有输出到终端? while(true) 到底阻塞了什么?</p>    <p>answer 2</p>    <p>因为此处直接阻塞了 <strong>事件循环</strong> ,还没开始,就已经被阻塞了</p>    <p>question 3,4</p>    <ol>     <li> <p>为什么 timeout1 中回调函数会阻塞 timeout2 中的回调函数的执行?</p> </li>     <li> <p>为何 timeout1 的计算密集型工作将会阻塞 timeout2 的回调函数的执行?</p> </li>    </ol>    <p>answer 3,4</p>    <p>因为该回调函数执行返回 <strong>事件循环</strong> 才会继续执行,回调函数将会阻塞事件循环的运行</p>    <p>question 5</p>    <p>为何读取文件的IO操作不会阻塞 timeout2 的执行?</p>    <p>answer 5</p>    <p>因为 IO 操作是异步的,会开启一个新的线程,不会阻塞到 <strong>事件循环</strong></p>    <p>参考文献:</p>    <ul>     <li> <p><a href="/misc/goto?guid=4959673487304952548" rel="nofollow,noindex">What exactly is a Node.js event loop tick?</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959673487386463005" rel="nofollow,noindex">What is the difference between a thread-based server and an event-based server?</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959673487464106127" rel="nofollow,noindex">Some confusion about nodejs threads</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959673487174767869" rel="nofollow,noindex">The JavaScript Event Loop: Explained</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959673487553754961" rel="nofollow,noindex">poll vs select vs event-based</a></p> </li>     <li> <p><a href="https://segmentfault.com/a/1190000003063859?utm_source=Weibo&utm_medium=shareLink&utm_campaign=socialShare#articleHeader0" rel="nofollow,noindex">Linux IO模式及 select、poll、epoll详解</a></p> </li>    </ul>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959673487647333502" rel="nofollow">https://segmentfault.com/a/1190000005173218</a></p>    <p> </p>