如果我们平常有浏览有关Node.js的文章,估计我们都会听到最多关于Node.js是异步非阻塞I/O,单线程,事件机制。本章节主要去深入探讨这几种特性(PS:本文是对所学知识的学习和总结,可能存在理解错误。请带着怀疑的眼光,同时如果有错误希望能指出。)
一、Node.js的异步I/O,非阻塞I/O
首先我们先来理解几个概念:阻塞IO(blocking I/O)和非阻塞IO(non-blocking I/O)、同步IO(synchronous I/O)和异步IO(synchronous I/O)。
问题:这里肯定有人想问,异步I/O和非阻塞I/O不是一回事吗??
答案:异步I/O和非阻塞I/O根本不是同一回事,曾经笔者一直天真的以为非阻塞I/O就是异步I/O T_T(直到看见朴灵大神的深入浅出Node.js)。
1.现在我们来了解什么是异步I/O,什么是同步I/O?
这里转自有趣的知乎er
老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1:老张把水壶放到火上,立等水开。(同步阻塞)老张觉得自己有点傻
2: 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3:老张把响水壶放到火上,立等水开。(异步阻塞)老张觉得这样傻等意义不大
4: 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
作者:愚抄
链接:https://www.zhihu.com/question/19732473/answer/23434554
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
二、单线程
在专题一的介绍,我们指导Node.js的runtime是V8,而V8设计是为了让Chrome浏览器对Javascript语言进行编译和解析的,另外有过JS经验的工程师都知道,JS的最大特点是单线程,而Node.js对V8的延用也是针对这一非常重要的特点。那么什么是单线程,单线程指的是,一个进程只能拥有一个线程,程序按顺序执行,只有等到前面的程序执行完毕,才能执行下一个!来看看Node对Http服务的模型,
Node.js单线程指的是主线程是单线程,由主线程按照编码顺序一步步执行,当主线程遇到阻塞的时候,后续的程序就会被卡住无法执行。说了这么多,不如来实践一下验真假:
先将index.js的代码改成这样,然后打开浏览器,你会发现浏览器在10秒之后才做出反应,打出Hello Node.js
JavaScript是解析性语言,代码按照编码顺序一行一行被压进stack里面执行,执行完成后移除然后继续压下一行代码块进去执行。上面代码块的堆栈图,当主线程接受了request后,程序被压进同步执行的sleep执行块(我们假设这里就是程序的业务处理),如果在这10s内有第二个request进来就会被压进stack里面等待10s执行完成后再进一步处理下一个请求,后面的请求都会被挂起等待前面的同步执行完成后再执行,所以这也说明Node.js单线程的执行模型,因为这样的特性,我们的页面不能有耗时很长的同步处理程序阻塞了程序的后续执行,而对于耗时过长的程序应该采用异步执行,这里也就是Node.js的第二个特性,异步。
三、异步
我们平时说的Node.js是异步的,那么具体是指那部分异步,而答案是主线程的异步处理函数队列+多线程异步I/O
1.主线程的异步处理函数队列
上面那句话看上去是不是有点抽象难懂,现在来进行解释,所谓主线程异步处理函数队列是指主线程的主要执行空间除了stack(执行栈)和head(产生堆)外,还会callback queue(回调函数队列),而callback queue是存放了异步处理的回调函数,当一个异步I/O处理完,就会向callback queue存放一个回调函数,当stack里面的程序执行完,主线程就会从callback queue取出已经存放好的回调函数去执行,而我们平时最常见的异步,除了事件外,还有timer,例如setTimeout,不如我们举一个栗子~
let sleep = (time)=>{
let exit = Date.now+time*1000;
while(Date.now()<exit){};
console.log('end sleep')
return
}
let main = ()=>{
setTimeout(()=>{
console.log('setTimeout run');
},0)
sleep(5);
console.log('after sleep');
}
main()
执行输出:
end sleep
after sleep
undefined(由于每个函数执行完都会自行return,如果没指定,就会输出undefined,这个是main执行完return出来的)
setTimeout run
下面是代码块的主线程堆栈执行:
看上图,主线程将main函数压进stack里面一行行解析执行,首先遇到setTimeout方法,因为setTimeout是一个异步处理函数,这里会setTimeout(callback,timeout),里面的callback函数移进callback queue里面,同时会把自己从主线程的stack里面移除,继续压进后面的执行代码来解析执行,这里继续压进sleep沉睡5s,接下来执行console,等到这里的同步代码执行完成后这个时候就会从callback queue里面取回调函数一个个执行。(题外话:就算setTimeout里面的timeout设置了是0,都是要等待执行块里面的同步代码执行完成后再去执行callback queue里面的代码)这就是异步里面的其一:主线程异步函数处理队列。(PS,setTimeout的回调函数的执行时间不是当前队列,而是下一个执行队列,即是是设置为,也最多是下一次执行队列第一个执行,详情阮一峰JS运行机制)
2.多线程异步I/O
这里可能有人会有疑问,买卖皮喔!你不是说Node.js是单线程吗,你这不是自己打脸吗?我想说,这里其实是没有冲突的,Node.js每个进程里面只有一个主线程来处理程序。因此,主线程是单线程,而主线程之外调用的I/O处理是通过一个叫做线程池的结构来管理的,所以I/O的处理是多线程的,而主线程和I/O线程池则通过上面刚刚讲述的主线程的异步处理函数队列来协作。(PSNode.js只对文件系统以及DNS实现了多线程I/O封装,网络I/O还是采用单线程形式)如图:
上图在主线程中,当遇到需要处理的I/O时,就将I/O的处理放在I/O线程池中管理,而主线程继续执行,当I/O线程池中有I/O完成了,就会想callback queue注册回调函数等待主线程执行,而Node.js的高性能也是得益于其将阻塞的I/O异步化,使得不影响主逻辑的执行。
四、事件驱动
文章至此,我们先进行总结,Node.js至此我们简介了两个主要特性,单线程,异步,每个Node程序只会在主线程中执行程序代码,在执行过程中将阻塞的I/O操作异步化,将放至I/O线程池中进行管理,当线程池中有I/O操作完成,就会向callback queue注册回调函数,等待同步逻辑执行完成后再通过callback queue里面取出回调函数压进stack里面执行,好了,而事件驱动的作用就是取出回调函数。事件驱动又叫事件循环,是指主线程从主线程的异步处理函数队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行。详情Node事件轮询
至此整个Node.js的异步化逻辑可以不断循环的跑起来了,以上则是我们日常所言的Node.js的三大特性以及其原理。