poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。(即本文的内容相关))
check: setImmediate() 回调函数在这里执行。
close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
ok, 这样就解释了Node是如何执行我们注册的事件, 那么还缺少一个环节, Node又是怎么把事件和IO请求对应起来呢? 这里涉及到了另外一种中间产物请求对象。
以打开一个文件为例子:
fs.open = function(path, flags, mode, callback){ //... binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback); }
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(oncomplete_sym, callback);
QueueUserWorkItem()方法接受3个参数:第一个参数是将要执行的方法的引用,这里引用的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操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到了异步的目的。
请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。
关于这一块其实个人认为不用过于细究, 大致上知道有这么一个请求对象即可, 最后总结一下整个异步IO的流程:
图引用自深入浅出NodeJs
至此, Node的整个异步Io流程都已经清晰了, 它是依赖于IO线程池epoll、事件循环、请求对象共同构成的一个管理机制。
Node为什么更适合IO密集
Node为人津津乐道的就是它更适合 IO密集型 的系统, 并且具有 更好的性能 , 关于这一点其实与它的异步IO息息相关。
对于一个request而言, 如果我们依赖io的结果, 异步io和同步阻塞io(每线程/每请求)都是要等到io完成才能继续执行. 而同步阻塞io, 一旦阻塞就不会在获得cpu时间片, 那么为什么异步的性能更好呢?
其根本原因在于同步阻塞Io需要为 每一个请求创建一个线程 , 在Io的时候, 线程被block, 虽然不消耗cpu, 但是其本身具有内存开销, 当大并发的请求到来时, 内存很快被用光, 导致服务器缓慢 , 在加上, 切换上下文代价也会消耗cpu资源 。而Node的异步Io是通过事件机制来处理的, 它不需要为每一个请求创建一个线程, 这就是为什么Node的性能更高。
特别是在Web这种IO密集型的情形下更具优势, 除开Node之外, 其实还有另外一种事件机制的服务器Ngnix, 如果明白了Node的机制对于Ngnix应该会很容易理解, 有兴趣的话推荐看这篇文章。
总结
在真正的学习Node异步IO之前, 经常看到一些关于Node适不适合作为服务器端的开发语言的争论, 当然也有很多片面的说法。
其实, 关于这个问题还是取决于你的业务场景。