Node.js中,最为人称道的就是单线程以及异步 I/O,其实对于单线程,熟悉前端的人应该不陌生。因为 js 在浏览器中运行时就是单线程。所以在 node.js中,单线程也仅仅是指 js ,而不是指全部的代码运行。
对于异步 I/O ,大多数的理解都是在 js 中,发起一个异步请求,然后node就会去执行,简单的理解是这个样子的,但是实际执行期间却没有这麽简单。
理想的异步 I/O
理想的异步 I/O 其实就跟我们理解的一样, js 发起一个异步请求,然后node去执行,但是并不会影响 js 后续代码的执行。当异步请求有结果返回时,node就会去执行这个异步请求的回调函数。这种处理在显示生活中是没有的,因为你没法用一个线程去完成所有的事情。所以 node 中的异步 I/O 是通过多线程进行模拟的,但是开启新的线程是 node 内部完成的,不需要你自己去操作。所以使用 node 的总体体验就是单线程加上异步。
node的线程池
node的线程池是由node官方维护和内部调用的,所以你的任务是定义你的异步任务,并且正确的传递你的回调函数。这里简单的说一下这个线程池是怎么实现调用的。
当你的 js 代码发起起步请求的时候,你的请求会经过一个叫 libuv 处理,这个东西的作用是判断你当前的操作系统,进行相关底层线程的调用,*nix平台的线程池是由node自己去定义的线程池,而 win 是调用 IOCP 进行的线程处理。
node的异步 I/O
1.事件循环
如果你了解过 node ,你对事件循环应该不陌生。因为这个是 node 的精髓所在,或者你知道的不是这个名字,他还有一个名字叫做事件轮询。他的意思就是说,发起一个异步 I/O,然后会有一个 while(true){},这样的死循环去不停地询问是否有 I/O 完成,如果有,就取出他的回调和执行结果执行,如果没有了事件要执行了,node就会推出这个进程,这个循环,循环一次得过程,叫做 Tick. 这里有必要说一下,这个循环是自 node 进程启动时就会创建,如果的程序是网络请求,正常情况下这个进程是不会退出的,除非你手动退出或者是出现异常。
2.观察者
在每个 Tick 的过程中,是如何判断是否有事件进行处理呢?这里引入观察者的概念。每个事件都会有一个或者多个观察者,而判断是否有事件需要执行就是向观察者询问当前事件是否要执行。
观察者的其实要做的事就相当于饭店的前台,厨房在一轮一轮的做菜,但是他们不知道要做什么菜,什么时候做。这时就需要去问前台,然后就知道了要做什么,如果没有客人点餐,就下班回家。这时,前台就是观察者,而厨房就是Tick。而前台是可以接受多个订单的,也就是说,一个事件观察者里是可以有多个事件的。
其实在浏览器中也是这么实现事件处理的。
3.请求对象
简单来说,这就是你发起异步 I/O 以后,node 将你的参数和回调封装起来,然后将 I/O 交给系统执行,最后系统将执行的结果及回调函数交给事件循环去执行。
简单说一下非阻塞 I/O 的执行过程,当发起 I/O 操作时,系统会立即返回一个文件描述符,然后你的线程会继续执行,而拿到文件你描述符以后,就是事件循环不停地拿着这个文件描述符去询问系统是否完成 I/O 操作,如果完成了,就拿回结果,将这个请求的状态标记为完成,执行回调。而如果没有完成,就会继续去询问是否完成。
4.执行回调
执行回调就是当 I/O 完成,事件的状态为完成时,Tick 就会将 I/O 执行的结果当成参数传递给回调函数,然后执行回调函数。
5.总结
以上只是最简单的描述了一下异步 I/O 的执行过程,真实情况比这个复杂了很多。但我们的几个关键字:单线程,事件循环,观察者和 I/O 线程池。这里的单线程和 I/O 线程池看起来是有些悖论的样子。这里是因为我们知道 js 是单线程的,按照常理来说他就不能充分的利用多核CPU。事实上,在Node中,除了 js 是单线程,node自身是多线程的。