底层操作系统,异步通过信号量、消息等方式有着广泛的应用。
PHP语言从头到尾都是以同步阻塞方式运行,利于程序员顺序编写业务逻辑。
异步I/O、事件驱动、单线程构成Node的基调。
why异步I/O
(1)、用户体验
在Web2.0中Ajax广泛应用异步刷新机制可以更好的提高用户体验,消除UI阻塞。后端同样采用异步I/O可以有效较少同时请求多个资源的效应时间其为max(M,N)。
(2)、资源分配
多任务主流方式:
a. 单线程异步I/O
b. 多线程并行
多线程的代价在于创建线程和执行期线程上下午切换的开销较大。在复杂场景中,多线程经常面临锁、状态同步的问题。但多线程可以更有效的利用多核CPU,提高利用率。
NodeJS利用单线程远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好利用CPU。
为了弥补单线程无法利用多核CPU特点,Node采用类似Web Workers的子进程。
异步I/O
操作系统内核对于I/O只有两种方式:阻塞和非阻塞
阻塞I/O一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。其造成CPU等待I/O,浪费时间和资源,CPU利用不充分。
非阻塞I/O不同之处在于调用之后会立即返回。其问题在于为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成,这种重复调用判定是否完成的技术叫做轮询
阻塞I/O会造成CPU等待浪费、非阻塞I/O需要轮询确认数据是否完全获取,让CPU处理状态判断,也会浪费CPU。轮询技术主要有以下几种:
(1) read。最原始效率最低的一种,重复调用来检查I/O的状态来完成完整的数据读取。
(2) select。在read上改进,通过对文件描述符上的事件状态进行判断。有个限制是由于采用1024长度的数组存储状态,最多可以同时检查1024个文件描述符。
(3) poll。采用链表的方式避免数组长度的限制,其次能避免不需要的检查。但文件较多的时候,性能还是十分低下。
(4) epoll。linux下效率最高的I/O事件通知机制,在进行轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将其唤醒。真实利用事件通知、执行回调的方式、而不是遍历查询。kequeue和epoll类似应用在FreeBSD下。
理想非阻塞异步I/O
多线程模拟理想非阻塞异步I/O,通过部分线程执行阻塞I/O或非阻塞I/O加轮询技术完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递。
glibc的AIO是典型的线程池模拟异步I/O,然而其存在一些难以忍受缺陷,不推荐。
libio是由libev的作者重新实现的一个异步I/O库,实质仍然是采用线程池与阻塞I/O模拟异步I/O。
IOCP是windows平台的异步I/O方案,提供理想的异步I/O:调用异步方法、等到I/O完成后通知、执行回调、用户无需考虑轮询。其内部仍然是采用线程池原理,不过是线程池由系统内核管理。
Node提供libuv作为抽象封装层,兼容windows平台和*nix平台。
Node是单线程,这里的单线程仅仅是指JavaScript执行在单线程中。在Node中,无论是windows或*nix平台,内部完成I/O任务另有线程池,只是对用户透明罢了。Node自身其实是多线程,只是I/O线程使用CPU较少,除了用户代码无法并行执行,所有的I/O则是并行执行。
Node的异步I/O
Node执行模型—事件循环
在进程启动的时候Node会创建一个类似while(true)的循环,每次执行一个循环体的过程称为Tick。每个Tick过程就是查看是否有事件待处理,若有则取出事件及其相关的回调函数,若存在关联的回调函数,则执行。
观察者
每个事件循环中有一个或多个观察者,判断是否有事件待处理的过程就是向这些观察者询问是否有要处理的事件。
事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等是事件的生产者、源源不断为Node提供不同类型的事件、这些事件被传递到观察者那里,事件循环从观察者那里取出事件并处理。在Windows下,这个循环基于IOCP创建,而在*nix则基于多线程创建。
请求对象
从JavaScript发起调用到内核执行完成I/O操作的过渡过程中,存在一个中间产物称为请求对象。
事件循环、观察者、请求对象和I/O线程池共同构成Node异步I/O模型的基本要素。
非I/O的异步API
setTimeout()和setInterval()分别用于单次和多次执行任务,其创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过则形成一个事件,其回调函数立即执行。
定时器的问题在于,它并非精确的,尽管事件循环十分快,但有可能某次Tick执行时间比较长。
process.nextTick()将回调函数放入到队列中,在下一轮Tick时取出执行,可以达到setTimeout(fn,0)的效果,由于不需要动用红黑树,效率更高时间复杂度为O(1)。
setImmediate()也是将回调函数延迟执行,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。由于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者,优先级如下idle观察者>I/O观察者>check观察者
process.nextTick()的回调函数保存在数组中,每次Tick会将数组中的回调函数全部执行;setImmediate()的回调函数保存在链表中,每次Tick只执行链表中的一个回调函数。