nodejs为什么可以处理高并发而不阻塞?
NodeJs是一个平台,构建在v8上(js语言解释器),采用事件驱动、
总所周知,NodeJs是用javascript语言开发的,javascript是一个单线程语言。当Node服务器收到成千上万计的并发请求的时候,却不会造成阻塞。原因就是nodeJs的事件驱动。
- 每个NodeJs进程只有一个主线程在执行程序代码,形成一个执行栈。
- 主线程之外,还维护了一个事件队列。当用户的网络请求或者其他的异步操作到来的时候,node都会把它放在Event Queue中,此时并不会立即执行它,代码也不会被阻塞,继续走下去,直到主线程代码执行完毕。
- 主线程的代码执行完毕之后,通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来取出第二个事件,再从线程池中分配一个线程去执行,然后第三个、第四个。主线程不断检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完毕了。此后每当有新的事件加入到事件队列中,都会通知主线程按顺序去取出交给Event Loop处理。当所有的事件执行完毕之后,会通知主线程,主线程执行回调,线程归还线程池。
- 主线程不断重复以上三步。
总结
我们看到的NodeJs单线程只是一个js主线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的I/O操作,从而实现异步非阻塞I/O,这便是node单线程的事件驱动的精髓了。
NodeJs中的事件循环的实现
NodeJs采用V8作为js的引擎,而I/O处理使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。
Event Loop的执行顺序
根据NodeJs的官方介绍,每次事件循环都包含了6个阶段:
- timers阶段:这个阶段执行timer(setTimeout、setInterval)的回调
- I/O callbacks阶段:执行一些系统调用错误,比如网络通信的错误回调
- idle,prepare阶段,仅node内部调用
- poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里
- check阶段:执行setImmediate()的回调
- close callbacks阶段:执行socket的close事件回调
setImmediate和setTimeout执行顺序的随机性
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
在浏览器中的setImmediate优先。因为浏览器中setTimeout有时间误差,即使setTimeout(fn, 0),实际上相当于setTimeout(fn, 4);
在node中的执行结果确是随机的。
node中的事件循环阶段
NodeJs中事件循环模型与浏览器相比大致相同,但是node中事件循环是分阶段的。
每个阶段都有一个先进先出的对调队列要执行。而每个阶段都有自己的特殊之处。简单的说,就是当事件循环进入到某个阶段之后,会执行该阶段特定的任意操作,然后才会执行这个阶段里面的回调。当队列被执行完,或者执行的回调达到上限之后,事件循环才会到下一个阶段。
timers
一个timer指定一个下限时间而不是准确时间,在达到这个下线时间后执行回调。在指定时间过后,timers会尽早执行回调,但是系统调度或者其他的回调的执行会延迟它。
从技术上讲,poll阶段控制timers什么时候执行,而执行的具体位置在timers。
I/O callbacks
这个阶段执行一些系统操作的回调,比如说TCP连接错误
idle,prepare
系统内部的一些调用
poll
这是最复杂的阶段
poll阶段有两个功能:一个是执行下限时间已经达到timers的回调,一是处理poll队列里面的事件
注:Node很多Api都是基于事件订阅完成的,这些api的回调应该都在poll阶段完成。
check阶段
执行setImmediate的回调
close callback阶段
执行socket的close事件回调