ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 异步式I/O与事件式编程 Node.js最大的特点是异步式I/O(非阻塞I/O)与事件紧密结合的编程模式,此模式与传统同步式I/O线性的编程思维不同,因为控制流很大程度要依靠事件和回调函数来组织,一个逻辑要拆分为若干单元。 <br> # 阻塞与线程 什么是阻塞(block)呢?线程在执行中若遇到磁盘读写或网络通信(统称为I/O操作),通常要耗费较长的时间,此时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让渡给其他工作线程,这种线程调度的方式称为阻塞。 当I/O操作完毕后,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式即传统的同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)。 <br> 异步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)则针对所有I/O操作不采用阻塞的策略。当线程遇到I/O操作时,不会阻塞的方式等待I/O操作的完成或数据的返回,而只是将I/O请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O操作时,以事件的形式通知执行I/O操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。 <br> 阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的CPU核心利用率永远是100%,I/O以事件的方式通知。 <br> 在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可让CPU资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js使用单线、非阻塞的事件编程模型。 <br> ![](https://box.kancloud.cn/8fbe2fd829ccf93a964517fd40b45936_765x766.png) 多线程同步式I/O <br> ![](https://box.kancloud.cn/ed790147ad1ff5983cae0b9cbf1a8be0_639x876.png) 单线程异步式I/O <br> 单线程事件驱动的异步式I/O比传统的多线程阻塞式I/O好在哪里呢?简而言之,异步式I/O就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换时还需执行内存换页,CPU的缓存被清空,切换回来时还要重新从内存中读取数据,破坏了数据的局部性。 ![](https://box.kancloud.cn/ab198635cdae5517a11fe7a0ada4d030_1000x291.png) <br> # 事件循环机制 Node.js在什么时候会进入事件循环呢?Node.js程序由事件循环开始到事件循环结束,所有的逻辑都是事件的回调函数,所以Node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行过程中,可能会发出I/O请求或直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直至程序结束。 <br> ![](https://box.kancloud.cn/72aacea80eaed32da2d7e0600797bdb5_749x862.png) 事件循环机制 <br> Node.js没有显式的事件循环,它对开发者不可见,由libev库实现。libev支持多种类型的事件,如ev_io、ev_timer、ev_signal、ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检测的事件监听器,直至检测不到时才退出事件循环,进程结束。 <br> # 单线程 Node.js保持了JS在浏览器中单线程的特点,在Node中JS与其余线程是无法共享任何状态的。单线程最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换带来的性能上的开销。 <br> 同样,单线程也有自身的弱点,具体表现在 * 无法利用多核CPU * 错误会引起整个应用退出,应用的健壮性值得考验。 * 大量计算占用CPU导致无法继续调用异步I/O <br> 像浏览器中的JS与UI公用一个线程一样,JS长时间执行会导致UI的渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续的异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。 最早解决这种大计算问题的方案是Google公司开发的Gears,它启用了一个完全能独立的进程,将需要计算的程序发送给这个进程,在结果得出后,通过事件将结果传递回来。这个模型将计算分发到其他进程上,以次来降低运算造成阻塞的几率。 <br> 后台H5制定了Web Workers的标准,Google放弃了Gears,全力支持Web Workers。Web Workers能够创建工作线程来进行计算,以解决JS大计算阻塞UI渲染的问题。工作线程为了不阻塞主线程,通过消息传递的方式来传递运行结果,这也使得工作进程不能访问主线程中的UI。 <br> Node采用了与Web Workers相同的思路来解决单线程中大量计算的问题(child_process)。子进程的出现,意味着Node可从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可将大量计算分解掉,然后在通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。通过Master-Worker的管理方式,也可很好地管理各个工作进程,以达到更高的健壮性。 关于如何通过子进程来充分利用硬件资源和提升应用的健壮性,这是一个值得探究的话题。 <br> <br> # 事件驱动模型 没有多线程额外工作,性能较高 每一个API都是异步执行,作为独立线程 ![](https://box.kancloud.cn/fc292fc53227c39e50f5786a947e88b7_1632x884.png) <br> <br> # 事件处理流程 1. 引入event对象,创建eventEmitter对象 2. 绑定事件处理程序 3. 触发事件 ``` // 引入event对象,创建eventEmitter对象 var events = require('events') var eventEmitter = new events.EventEmitter() var connectHandler = function () { console.log('connected') } // 绑定事件处理程序 eventEmitter.on('connection', connectHandler ) // 触发事件 eventEmitter.emit('connection') console.log('程序执行完毕') ```