当前位置: 首页 > 后端技术 > Node.js

Nodejs高性能原理(上)---异步非阻塞事件驱动模型

时间:2023-04-03 20:14:20 Node.js

系列文章Nodejs高性能原理(上)---异步非阻塞事件驱动模型Nodejs高性能原理(下))---EventCycleDetails前言我终于开始了我的nodejs博客生涯,让我先从基本原理说起。之前写过一篇关于浏览器执行机制的文章,和nodejs还是有很多相似之处的。不熟悉的可以先去看看。Javascript执行机制--单线程、同步异步任务、事件循环写下来可能还是有点乱,后面补充,欢迎指正,尤其是翻译文章。.因为懒得做,全文图片要么来自官网,要么来自百度图片。补充:当前Nodejs版本10.3.0PS:2019/8/13修改部分说明。什么是节点?在官网,是:Node.js是一个基于ChromeV8引擎的JavaScript运行环境。Node.js使用事件驱动、非阻塞I/O模型,使其轻量且高效。Node.js的包管理器npm是世界上最大的开源库生态系统。一三个我就不说了,就是外在条件因素,我们重点了解第二个。什么是非阻塞I/O?摘自<>操作系统将计算机抽象,将所有输入输出设备抽象为文件。内核在进行文件I/O操作时,通过文件描述符进行管理,文件描述符类似于应用程序与系统内核之间的凭证。如果应用程序需要进行I/O调用,需要先打开文件描述符,然后根据文件描述符实现文件的数据读写。这里非阻塞I/O和阻塞I/O的区别在于阻塞I/O完成了获取数据的整个过程,而不是BlockingI/O没有数据直接返回。要获取数据,需要通过文件描述符再次读取。I/O是指磁盘文件系统或数据库的写入和读取。有些名词听上去像异步、非阻塞、同步、阻塞好像是一回事,从实际效果上看好像是一回事,但从计算机内核上来说真的不是一回事我/O。为了更全面的说明这一点,我们可以一一列举,分别是:阻塞式I/O(BlockingI/O)会在发起I/O操作后阻塞进程,直到得到响应后才进行其他操作或超时;例:调用一个I/O/O操作API请求(如读写操作),必须等待系统内核级完成所有操作,如寻盘、读取数据、拷贝数据到内存等。优点:基本不占用CPU资源,可以保证操作结束或数据返回;缺点:单进程单请求,阻塞造成不必要的CPU等待,不能充分应用;非阻塞I/O(Non-blockingI/O):发起一个I/O操作不等待响应或超时立即返回让进程继续执行其他操作;示例:调用API请求进行I/O操作(如读写操作)时,不等待系统内核级完成所有操作,如寻盘、读取数据、拷贝数据到内存等。返回后等待;优点:提高性能,减少等待时间;缺点:只返回当前调用状态,如果想获取完整数据,需要反复请求判断操作是否完成,造成CPU损耗。基本方法是轮询;I/O多通道多路复用(I/OMultiplexing)在并发量大的时候肯定是不适用的,于是多路复用应运而生,select有几种模式来添加需要I/O操作的socketselectListenin,然后阻塞线程,等待操作完成或者超时后,select系统被激活回调,线程发起I/O操作。具体的方式是轮询所有的socket,因为单进程支持最大文件描述符为1024,所以实际并发低于这个数优点:同一个线程可以进行多次I/O,跨平台支持缺点:原则上还是阻塞的,单个I的处理时间/O甚至比阻塞I/O更需要轮询,并发量有限(1024);poll类似于select机制,但是poll是基于链表实现的,没有并发量的限制。优点:同一个线程可以进行多次I/O,并发量没有限制。缺点:仍然是遍历链表校验,效率低;epoll改进了前两者的缺点,使用回调通知机制减少内存开销,不会因为大并发而降低效率。Linux下最高效的I/O事件机制优点:同一个线程可以进行多次I/O,并发远超1024而不影响性能缺点:并发小的时候效率可能不如前两者;信号驱动I/O(Signal-drivenI/O)应用使用socket进行信号驱动I/O,并安装一个信号处理函数,进程继续运行,不会阻塞,当数据准备好后,进程会收到一个SIGIO信号,在信号处理函数中可以调用I/O操作函数来处理数据。优点:执行后不需要阻塞进程,收到信号后才进行操作,提高资源利用率。缺点:并发量大时,可能会因为信号队列溢出而无法通知;同步I/O(SynchronousI/O):发起I/O操作后,进程将被阻塞,直到收到响应或发生超时。按照这个定义,前面提到的阻塞I/O、非阻塞I/O、I/O多路复用、信号驱动I/O都是同步I/O。无论是等待所有操作完成,还是通过轮询获取操作结果等,进程都会被阻塞。区别无非就是中间的等待时间如何分配;优点:执行顺序一目了然;缺点:阻塞造成不必要的CPU浪费等待或冗余查询不能完全应用;异步I/O(AsynchronousI/O):直接返回继续执行下一条语句,当I/O操作完成或数据返回时,以事件的形式通知Process进行IO操作。注意:异步I/O和信号驱动I/O不同于异步阻塞和非阻塞。前者在I/O操作完成时通知进程,后者在可以发起I/O操作时通知进程;优点:无需等待,无需查询,提高性能,会有通知信息;缺点:代码读取和过程控制比较复杂;(本来想直接上这里的,但是相似度太高,容易模糊,所以打算画图,因为太多懒得说了,百度找了张图,还有然后就找不到了。最后在一篇文章里找到了比较清晰的原理图,厚颜无耻礼貌的借用)流程图来自IO——同步、异步、阻塞、非阻塞简述:阻塞与阻塞的区别I/O与非阻塞I/O是:在I/O操作完成或数据返回前是否等待或返回!(可以理解为一直等待或者分时间段等)同步I/O和异步I/O的区别在于:进程是否会在I/O操作完成之前阻塞,或者数据返回(或主动查询或被动查询)等待通知)!一个活生生的例子正在等待外卖吧阻塞I/O:白领A在下单后等待前台服务员,直到他收到外卖才离开,他身后的其他人正在排队等候离开;非阻塞I/O:白领B下单后,每隔一段时间就问前台服务员外卖好了没有。我需要来回多次排队,但妨碍其他人的时间更少;I/O多路复用:白领A和B在两个前台A服务员下单,厨房大叔先把外卖送到对应的服务员;信号驱动I/O:白领C要下单,前台服务员会先询问厨房里有没有食材,得到回复后再帮忙。白领C下单;异步I/O:白领E下单取号,然后去做其他事情,直到前台服务员叫号,告诉他外卖准备好了;为什么Nodejs这么提倡非阻塞异步I/O?用户体验我们都知道,Javascript在浏览器中是单线程执行的。JS引擎线程和GUI渲染线程是互斥的。如果您以同步方式加载资源,则UI会停止渲染并且无法交互。你认为用户会做什么?而使用异步加载就没有这个问题。这不仅是阻塞时的体验问题,也是加载时间的问题。例如,两段I/O代码执行分别需要时间a和b。一般:同步执行上线时间:a+b;异步执行时间:Math.max(a,b);这就是为什么异步非阻塞I/O是nodejs的主要概念,因为I/O非常昂贵。主流的资源分配方式有两种:单线程串行执行优点:执行顺序一目了然;缺点:不能充分利用多核CPU;多线程并行处理优势:有效利用多核CPU;缺点:创建/切换线程代价高昂,存在锁和状态同步等复杂问题;Nodejs方案:单线程事件驱动,非阻塞I/O优点:避免锁和状态同步等复杂问题,可以提高CPU利用率;事件驱动事件是一种监听事件或状态变化为执行回调函数的流程控制方法,一般步骤是确定响应事件的元素;确定需要为指定元素响应的事件类型;为指定元素的指定事件编写相应的事件处理器;将事件处理程序绑定到指定的元素。指定事件;让我们以创建一个必须为每个条目学习的服务器为例http.createServer((req,res)=>{letdata='';req.on('data',chunk=>(data+=chunk));req.on('end',()=>{res.end(data);});}).listen(8080);所谓事件驱动就是在nodejs中有一个事件队列,每一个传入的请求处理完之后就会关闭,然后继续为下一个请求服务。当请求完成后,会被推入处理队列,然后循环查看队列事件是否发生变化。如果存在,则执行相应的回调函数。如果没有,它会跳到下一步来回。(看我在runoob看到的图,不小心又借用了)事件驱动是非常高效和可扩展的,因为请求总是被接受而不等待任何读写操作,更详细的内容下面会讲到异步nodejs的I/O实现。这个知识点是从<>看的。共同构成Node异步I/O模型的四个基本元素:事件循环、观察者、请求对象、执行回调。(由于底层语言和系统实现不同,只能根据内容简单说一下流程,其他的也无能为力了。)事件循环流程启动后,node会创建一个循环。每次执行循环体的过程称为Tick。每个Tick的过程就是看是否有事件需要处理,如果有事件及其相关的回调执行,则重复该Tick,否则退出流程。(百度搜到<<nodejs简介>>书中示意图)ObserverNode.js在设计模式中基本上所有的事件机制都是用观察者模式实现的。每个事件循环中都有一个或多个观察者。通过询问这些Observers可以知道是否有事件需要处理。浏览器中的事件可能来自界面交互或文件加载,而Node主要来自网络请求、文件I/O等,这些事件都有对应的观察者。(window下基于IOCP创建,*nix下基于多线程创建)request对象对于Node中的异步I/O调用,从发起调用到执行I/O的过渡过程中有一个中间产物/O由内核请求对象操作。在Javascript层面,代码会调用C++核心模块,核心模块会调用内置模块,通过libuv进行系统调用。创建一个request对象,将入参和当前方法的所有状态封装在request对象中,包括发送到线程池等待执行和I/O操作完成后的回调处理。然后被压入线程池等待执行,Javascript调用返回继续当前任务的后续操作。第一阶段完成。(官方介绍:libuv是一个多平台的支持库,专注于异步I/O,主要是为Node.js使用而开发的,相当关键操作调用完成后,会保存结果,然后执行状态会提交给IOCP(记得窗口是基于IOCP创建的)通知当前对象操作完成,线程会返回线程池。事件循环的观察者也是中间使用的时候,每个Tick都会调用IOCP相关的方法检查线程池是否有完成请求,如果有则将请求对象添加到I/O观察者的队列中作为事件处理。至此,整个异步I/O流程结束,完整流程如下(百度在《深入浅出nodejs》一书中找到原理图)参考资源《深入浅出nodejs》runoobIO——同步、异步、阻塞、非阻塞(补救文章)