异步IO实现现状
- I/O的阻塞与非阻塞:IO对于操作系统内核而言,只有阻塞与非阻塞两种方式。阻塞模式的I/O会造成应用程序等待,直到I/O完成,会造成CPU等待IO,浪费等待时间,CPU的处理能力不能充分利用。同时操作系统也支持将I/O操作设置为非阻塞模式,这时应用程序的调用将可能在没有拿到真正数据时就立即返回了,为此应用程序需要多次调用才能确认I/O操作完全完成,但由于IO并没有完成,立即返回的并不是业务层期望的数据,仅仅是当前调用的状态。
- I/O的同步与异步:I/O的同步与异步出现在应用程序中。如果做阻塞I/O调用,应用程序等待调用的完成的过程就是一种同步状况。相反,I/O为非阻塞模式时,应用程序则是异步的。
堵塞I/O造成CPU的等待浪费,非堵塞I/O是需要轮询去确认是否完成数据获取,让CPU处理状态判断,这是对cpu资源的浪费。
轮询的演进,以减少I/O状态判断的CPU损耗
- read:性能最低的一种,需要重复调用来检查I/O状态,cpu一直耗在等待上
- select: 在read基础上的改进方案,通过对文件描述符上的事件状态进行判断(一次只能检查1024文件描述符)
- poll: 较select有所改进,采用链表的方式避免数据长度的限制,其次它能避免不必要的检查。但在文件描述符过多时,性能十分低下。
epoll: 该方案是Liunx 下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会休眠,直到事件将它唤醒。它是真实利用来事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CUP。但是休眠期间CPU是闲置的,对于当前线程而言利用率不够。
轮询技术满足来非堵塞I/O确保获取完整数据的需求,但是对于应用程序而言,依然是一种同步,因为应用程序依然需要等待I/O完全返回,依旧花费很多时间等待。CPU要么用于遍历文件描述符,要么处于休眠等待事件发生
理想的非堵塞异步I/O
应用程序发起非堵塞调用,无需遍历或者唤醒,可以直接处理下一个任务,只需要在I/O完成后通过信号或者回掉将数据传递给应用程序
现实中的异步I/O
线程池模拟异步I/O。
通过让部分线程用轮询技术进行数据的获取,让一个线程进行计算处理,通过线程间的通信将I/O得到的数据进行传递
node的上一层libuv 即用此实现,并进行来平台的兼容
node 的异步I/O
事件循环
进程启动时,Node会创建一个类似while(true)的循环,判断是否有事件需要处理,若有,取出事件并执行回调函数。
观察者
在Tick 的过程中,引入观察者的概念,用来判断是否有事件需要处理。一个事件循环中有一个或多个观察者,一个观察者里可能有多个事件
请求对象
从javaScript 调用node的核心模块,核心模块调用C++的内建模块,内建模块通过libux 进行系统调用,这是node的经典调用方式。
javaScript 传入的参数和当前的方法都会被封装到这个请求对象中去,回调函数被设置到这个对象的oncomplete_sym属性上
执行回调
组装好请求对象、送入I/O线程池等待执行完成异步I/O的第一部分,回调通知是第二部分,线程池中的I/O操作调用完毕之后,会把结果存储在req->result属性上,然后调用PostQueuedCompletionStatus()通知IOCP,告知当前对象操作已经完成,
PostQueuedCompletionStatus()方法的作用是向IOCP提交执行状态,并将线程池归还线程,通过这个方法提交状态可GetQueuedCompletionStatus提取,这个工程中动用了事件循环的I/O观察者,每次Tick的执行中,都会调用IOCP相关的GetQueuedCompletionStatus方法检查线程池中是否有执行完毕的请求,如果有就将请求对象加入I/O观察者队列中,然后当作事件处理,I/O观察者调用回调函数就是取出请求对象的result属性作为参数,取出oncomplete_sym作为方法,然后执行。
非I/O的异步API
setTimeout()、setInterval()、setImmediate()、process.nextTick()
它们与异步I/O相似,只不过不需要I/O线程池的参与,每次Tick时。会检查是否满足条件,如果满足就形成一个事件,所有事件都会被事件循环按顺序排队处理,直到所有队列为空。
在nodejs中有多个观察者队列,不同类型的事件在它们自己的队列中排队。在处理一个类型的队列之后,在进入到下一个队列之前,事件循环将处理两个中间队列(宏任务队列和微任务队列),直到中间队列为空。
- 宏任务 setTimeout()、setInterval()、setImmediate()
优先级:主代码块 > setImmediate > setTimeout / setInterval - 微任务 process.nextTick(),promise.then()
优先级:process.nextTick > Promise
执行顺序
1、先执行微任务队列,并且一次执行完。
2、在执行宏任务一个宏任务如果发现还有微任务继续执行微任务