谈节点异步IO和事件循环
前言
学习Node时无法避免异步IO,异步IO与事件周期密切相关。然而,这件作品并没有被仔细理解和整理。就在最近,我在做项目的时候,有了一些想法,录了下来。希望能尽可能的整理出这一块的知识。如有错误,请指点,轻喷~ ~
一些概念
同步异步阻塞非阻塞
查阅数据时,我们发现很多人对异步和非阻塞的概念很困惑。事实上,他们完全不同。同步异步指的是两者之间的关系,阻塞非阻塞指的是状态。
以前端请求为例,很多人应该编写以下代码
$.Ajax (URL)。成功(()={ 0.//做某事})是同步和异步的
如果是同步的,应该是客户端发起请求后,直到服务器处理完请求,才会返回继续执行后续逻辑,使客户端和服务器保持同步状态。
如果是异步的,应该在客户端发起请求后立即返回,但是请求可能不会到达服务器,或者请求正在处理中。当然,在异步情况下,客户端通常会注册一个事件来处理请求完成后的情况,比如上面的succeed函数。
阻塞非阻塞
首先,我们需要理解一个概念。Js是单线程,但浏览器不是。事实上,您的请求是浏览器运行的另一个线程。
如果被阻塞,线程将不会被释放用于其他请求,直到请求完成。
如果它是非阻塞的,那么线程可以启动请求并继续做其他事情,而不用等待请求完成。
摘要
之所以经常混淆,是因为不清楚讨论的是哪个部分(下面提到),所以同步和异步讨论的对象是双方,阻塞和非阻塞讨论的对象是本身。
输入输出和中央处理器
Io和Cpu可以同时工作。
输入输出:
I/O(英文:Input/Output),即输入输出,通常是指内部存储器与外部存储器或其他外围设备之间的数据输入输出。
中央处理器
用计算机软件解释计算机指令和处理数据。
节点中的异步输入输出模型
IO分为磁盘IO和网络IO,有两个步骤
等待数据准备就绪,将数据从内核复制到进程节点中的磁盘Io
以下讨论基于*nix系统。
理想的异步Io应该如上所述,如图所示:
事实上,我们的系统无法完美地实现这样的调用模式。节点的异步Io,比如读文件,是通过线程池实现的。可以看到,Node通过另一个线程执行IO操作,完成后再通知主线程:
另一方面,在窗口下,使用IOCP接口完成。从用户的角度来看,IOCP确实是一个完美的异步调用模式,但实际上它也使用内核中的线程池。后者与nix系统的区别在于线程池由用户层提供。
节点中的网络输入输出
在进入主题之前,我们先了解一下Linux的Io模式。在这里,我们建议您阅读这篇文章,总结如下:
I/o阻塞(阻塞io)
因此,阻塞IO的特点是在IO执行的两个阶段都是阻塞的。
输入输出(非阻塞输入输出)
当用户进程发出读操作时,如果内核中的数据没有准备好,它不会阻塞用户进程,而是会立即返回一个错误。从用户进程的角度来看,它发起读操作后,不需要等待,而是立即得到一个结果。当用户进程判断结果是错误时,它知道数据没有准备好,所以它可以再次发送读取操作。一旦内核中的数据准备好,并且再次接收到用户进程的系统调用,它会立即将数据复制到用户内存中,然后返回。
输入输出多路复用
因此,I/O复用的特点是一个进程可以通过一种机制同时等待多个文件描述符,这些文件描述符(socket descriptors)中的任意一个进入读就绪状态,select()函数可以返回。
异步输入输出
用户进程启动读取操作后,可以立即开始做其他事情。另一方面,从内核的角度来看,当它接收到异步读取时,它会立即返回,因此不会对用户进程产生任何阻塞。然后,内核将等待数据准备完成,然后将数据复制到用户的内存中。当所有这些完成后,内核将向用户进程发送一个信号,告诉它读取操作已经完成。
节点采用输入输出复用方式,输入输出复用方式有读取、选择、轮询、epoll等几种子方式。节点采用最优的epoll模式。在这里,让我们简单地谈谈差异,并解释为什么epoll是最好的。
阅读
阅读.它是性能最低的最原始的一个,它会反复检查I/O的状态,完成数据的完整读取。在获得最终数据之前,中央处理器一直用于反复检查输入/输出状态。图1是通过读取进行轮询的示意图。
挑选
选择.它是一种基于读取的改进方案,通过判断文件描述符上的事件状态。图2是通过选择进行轮询的示意图。Select polling有一个较弱的限制,即它使用一个1024长度的数组来存储状态,这意味着它可以同时检查多达1024个文件描述符。
投票
民意测验.Poll是对select的改进,它使用链表来避免数组长度的限制。其次,可以避免不必要的检查。然而,当有许多文件描述符时,它的性能非常低。
使用
该方案是Linux下最高效的I/O事件通知机制。如果进入轮询时没有检测到输入/输出事件,它将休眠,直到事件发生并唤醒它。它真正使用事件通知来执行回调,而不是遍历查询,所以不会浪费CPU,执行效率高。
此外,其他民意测验和选择有以下缺点(引自文章):
每次调用select时,都需要将fd集从用户模式复制到内核模式。当有很多fds时,这种开销会很大。同时,每次调用select时,都需要遍历内核中传入的所有fds。当有很多fds时,这种开销也很大。select支持的文件描述符数量太少。默认值为1024epoll。
Epall是select和poll的改进,所以应该可以避免以上三个缺点。那么epoll是如何解决的呢?在此之前,让我们看看epoll和select和poll之间的区别。选择和轮询仅提供一个函数——select或poll函数。Epall提供三个函数,epoll_create、epoll_ctl和epoll_wait。epoll_create就是创建一个epoll句柄。Epoll_ctl是注册监听的事件类型;Epoll_wait是等待事件生成。
对于第一个缺点,epoll的解决方案是在epoll_ctl函数中。每次在epoll句柄中注册一个新事件(epoll_ctl _ add是在epoll_ctl中指定的),所有fd都会被复制到内核中,而不是在epoll_wait时被重复复制。Epoll确保每个fd在整个过程中只复制一次。
对于第二个缺点,epoll的解决方案不像select或poll那样每次轮流向fd的等待队列中添加current,而是在epoll_ctl处仅挂起current一次(这一次是必要的),并为每个fd分配一个回调函数。当设备就绪并唤醒等待队列时,将调用该回调函数,该回调函数将把就绪fd添加到就绪链表中)。Epoll_wait的工作实际上是检查这个ready链表中是否有ready fd(使用schedule_timeout()来实现睡一会判断一会的效果,类似于select实现中的步骤7)。
对于第三个缺点,epoll没有这个限制。它支持的FD限制是可以打开的最大文件数。这个数字通常比2048大得多。例如,在1GB内存的机器上,它大约是100,000。一般来说,这个数字与系统内存有很大的关系。
Node中的异步网络Io由epoll实现。简单来说就是用一个线程管理多个IO请求,通过事件机制实现消息通信。
事件循环
在了解了Node中磁盘IO和网络IO的底层实现后,基于上面的代码可以看出,Node基于事件注册完成Io后会进行一系列的处理,其内部利用了事件循环的机制。
事件循环是指每次执行同步任务后,JS都会检查执行栈是否为空,如果是,则执行注册的事件列表,并持续循环进程。节点中的事件周期有六个阶段:
每个阶段都处理相关事件:
Timers:执行setTimeout和setInterval中到期的回调。待定回调:执行一个输入/输出回调,该回调被延迟到下一次循环迭代。闲置,准备:仅在系统内部使用。轮询:检索新的输入/输出事件;执行I/O相关的回调(几乎在所有情况下,除了关闭的回调函数,由timer和setImmediate()调度),其余情况在这里都会被阻塞。(即本文内容相关))这里执行的是check: setImmediate()回调函数。Close callback:执行Close事件的回调,例如socket.on('close'[,fn])或http.server.on('close,fn)。好的,这解释了节点如何执行我们注册的事件,但是仍然缺少一个链接,那么节点如何将事件与IO请求相匹配呢?这里涉及到另一个中间请求对象。
以打开文件为例:
fs.open=函数(路径、标志、模式、回调){//.binding.open(路径模块。_makeLong(路径)、stringToFlags(标志)、模式、回调);}fs.open()用于根据指定的路径和参数打开文件,从而获得文件描述符,这是所有后续I/O操作的初始操作。从前面的代码可以看出,JavaScript代码通过调用C核心模块来执行低级操作。
从JavaScript调用Node的核心模块,核心模块调用C的内置模块,内置模块通过libuv调用系统,这是Node中的经典调用方式。在这种情况下,libuv被用作封装层,有两种平台实现,本质上是调用uv_fs_open()方法。在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法封装在这个请求对象中,我们最关心的回调函数设置在这个对象的oncomplete_sym属性上:
req _ wrap-object _ Set(on complete _ sym,回调);QueueUserWorkItem()方法接受三个参数:第一个参数是要执行的方法的引用,这里指的是UV _ FS _ Thread _ proc第二个参数是uv_fs_thread_proc方法运行所需的参数;第三个参数是执行标志。当线程池中有可用的线程时,我们调用uv_fs_thread_proc()方法。uv_fs_thread_proc()方法根据传入参数的类型调用相应的基础函数。在uv_fs_open()的情况下,实际上调用的是fs_open()方法。
此时,JavaScript调用立即返回,由JavaScript层发起的异步调用的第一阶段结束。JavaScript线程可以继续执行当前任务的后续操作。当前的输入/输出操作正在线程池中等待执行。无论是否阻塞I/O,都不会影响JavaScript线程的后续执行,从而达到异步的目的。
请求对象是异步I/O过程中的重要中间产品,所有的状态都存储在这个对象中,包括发送到线程池执行和I/O操作后的回调处理。
其实我个人觉得这一块没必要研究的太仔细。我一般都知道有这样的请求对象。最后,我将总结整个异步IO过程:
该图以简单的方式引用了NodeJs
至此,Node的整个异步IO过程已经明确,是一个由Io线程池epoll、事件循环和请求对象组成的管理机制。
为什么节点更适合输入输出密集型
Node更适合IO密集型系统,性能更好,这与其异步IO密切相关。
对于一个请求,如果我们依赖io的结果,异步io和同步阻塞io(每个线程/请求)在io完成之前不能继续执行,而同步阻塞io一旦阻塞就不会得到cpu时间片,那么为什么异步更好呢?
根本原因是同步阻塞Io需要为每个请求创建一个线程。在Io期间,线程是block,它不消耗cpu,但它有自己的内存开销。当大型并发请求到达时,内存会很快耗尽,从而导致服务器变慢。此外,上下文切换的成本也会消耗cpu资源。Node的异步Io由事件机制处理,不需要为每个请求创建一个线程,这就是Node性能更高的原因。
尤其是在Web IO密集型的情况下,它更有优势。除了Node,其实还有另外一个带有事件机制的服务器Ngnix。如果了解Node的机制,对于Ngnix来说应该很容易理解。如果你感兴趣,建议你阅读这篇文章。
摘要
在真正学习Node的异步IO之前,我们经常会看到一些关于Node是否适合作为服务器端开发语言的争论。当然,也有很多片面的说法。
其实这个问题还是要看你的业务场景。
假设您的业务是cpu密集型的,那么使用Node进行开发绝对不适合。为什么不呢?因为Node是单线程的,当你被阻塞的时候,其他的事件就做不了了,请求也处理不了,回调也处理不了。
Node在IO密集型方面是否优于Java?其实不一定要看你的生意。如果你的业务非常并发,但是你的服务器资源有限,就像现在有一个入口一样。Node一次可以进10个人,而Java一个人依次排队。如果10个人同时进入,当然Node更有优势,但是如果有100个人(比如1w异步请求),那么Node会因为其异步机制导致应用被挂起,内存暴涨,IO被阻塞,这是不可能的。另一方面,Java可以有序地处理它,尽管会慢一些。然而,一台服务器故障造成的在线事故损失是不可估量的。(当然,如果服务器有足够的资源,Node可以处理。).
最后,其实Java也是一个异步IO的库,但是相对来说Node的语法更自然,更接近,更适合。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。
版权声明:谈节点异步IO和事件循环是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。