Node.js异步IO
Node.js异步IO
异步早就存在于操作系统的底层,通过信号量、消息等方式应用。
与Node的事件驱动、异步IO设计理念比较相近的一个产品是Nginx。
为什么要异步IO
原因:Node是面向网络设计的。
网络请求是一个耗时操作,同步网络请求会导致程序处于等待卡死状态,用户体验差。
计算机将组件进行了抽象,分为IO设备和计算设备。
处理一组任务的方案:
单线程串行执行:易于编写,但是性能差。
多线程并发执行:性能一般较好;但多线程开销大,且需要处理锁、状态同步等问题,编写难度大。
Node的方案是:利用单线程避免死锁、同步等问题;利用异步IO让单线程不会阻塞,更好地利用CPU。
异步IO的现状
异步IO不是Node首创。
异步/同步和阻塞/非阻塞是两回事。
操作系统内核对于IO只有两种方式:阻塞与非阻塞。
阻塞IO:操作系统处理IO时,会等待IO结束,获得数据后再返回。
非阻塞IO:操作系统启动IO后,立即无数据返回。
操作系统把输入输出设备抽象为文件,在进行IO操作时,通过文件描述符进行管理,先打开文件描述符,在根据文件描述符进行数据的读写。
非阻塞IO返回后,CPU的时间片段可以用来处理其他事务,性能更好,但又一个问题:如何获取IO的完整数据?应用程序需要重复调用IO操作来确认IO是否完成,称为“轮询”技术。这也会浪费CPU资源。
现存的轮询技术:
read
select
poll
epoll:Linux系统,IO事件通知机制
kqueue:FreeBCD系统
理想的非阻塞异步IO
应用程序发起非阻塞调用,无需轮询,IO完成后,系统内核通过事件或信号将数据传递给应用程序。
现实的异步IO
现实:通过线程池模拟异步IO。让一个线程进行计算处理,让部分其他线程进行阻塞IO或非阻塞IO加轮询技术来完成数据,通过线程间通信将IO得到的数据传递给计算线程。
Windows系统:IOCP
Linux系统:自定义线程池
IO包括:文件读写、硬件、套接字等。
在Node中,所谓的单线程只是JS执行在单线程,完成IO任务的另有线程池。
Node的异步IO
Node的异步IO模型:
事件循环
观察者
请求对象
IO线程池
事件循环
在进程启动时,Node就会创建一个死循环,每执行一次循环称为“Tick”。
观察者
在每个Tick中,判断是否有事件需要处理。每个循环中有一个或多个观察者,观察者收集事件,对事件进行分类,事件循环则从观察者中获取事件并处理。
请求对象
当JS发起一个异步调用时,Node会创建一个“请求对象”,封装参好数和回调,然后推入线程池中等待执行,至此,JS调用返回,继续执行后续JS代码。
线程池中的IO操作调用完毕后,会把获得的数据存储到请求对象的result属性中,然后通知IOCP记录状态,并释放线程。
在每次Tick时,事件循环的IO观察者会检查IOCP的状态,如果有已完成的请求,就会把这些请求对象加入到观察者的队列中,等待被取出当做事件处理。
回调函数的执行:取出请求对象,提取请求对象的result和oncomplete_sym属性,执行oncomplete_sym(result)
非IO的异步API
Node中存在一些与IO无关的异步API。
定时器
setTimeout()
setInterval()
不需要IO线程池参与。
流程:创建定时器,插入到“定时器观察者”内部的红黑树中,每次Tick,都会检查定时器对象是否超时,超时就会调用定时器的回调函数。
存在的问题:不精确。
process.nextTick()
调用定时器需要动用红黑树、创建对象、迭代等操作,比较浪费性能。
想要立即异步执行一个任务,使用 process.nextTick() 方法。该方法只会将回调函数放入队列中,在下一次Tick中取出执行。
setImmediate()
延迟执行,和 process.nextTick() 类似。但 process.nextTick() 会优先执行。
process.nextTick() 的回调保存在一个数组中,每轮Tick会全部调用。
setImmediate() 的回调则是保存在一个链表中,每轮Tick只会调用一个。

