异步I/O
什么是异步I/O
资源请求的开始不依赖一上一个资源请求的结束,即第一个资源的获取并不会阻塞第二个资源,第二个资源的请求并不依赖第一个资源的结束。
getData('from_db',function(res){
// 消费时间M
})
getData('from_remote_api',function(res){
// 消费时间N
})
- 异步I/O时间:
- 同步I/O时间:
异步I/O减少了资源请求的整体时间,用户体验佳、性能高
非阻塞I/O
- 阻塞I/O : 当一个I/O任务上处理机后,会阻塞任务队列,CPU会等待I/O的全部系统操作完成
- 非阻塞I/O :非阻塞调用后不带数据直接返回,获取数据通过文件描述符再次读取,让CPU的时间片可以用来处理其他事务
由于非阻塞I/O并没有完全获取到业务层需要的数据,需要不断的
轮询
来确认I/O操作是否完成
轮询
轮询技术
- read : 最原始、最低效,重复调用来检查I/O状态来完成完整数据的读取。获取最终数据前,CPU大量时间会浪费在等待上
- select :read的基础上改进,通过文件描述符上的事件状态来进行判断。状态存储采用1024长度的数组
- poll :select基础上改进,采用链表的方式避免数组长度的限制,避免不需要的检查。当文件描述符较多时,性能还是十分低下
- epoll :利用事件通知机制,在进入轮询的时候如果没有检查到I/O事件会进行休眠,事件发生时唤醒。事件通知、执行回调而不是循环查询,不浪费CPU、效率高
轮询技术满足了非阻塞I/O获取完整数据的需求,但对于应用程序而言,它仍然只能算一种同步。我们期望的时无须通过遍历或者事件唤醒等方式的轮询直接处理下一个任务
现实中的异步I/O
- *nix : 起初libeio配合libev实现异步I/O,Node自行实现了线程池来完成异步I/O
- win : IOCP调用异步方法,等待I/O完成后的通知,执行回调,用户无须考虑轮询
由于平台之间的差异,Node提供了libuv
作为抽线层封装,平台的兼容由这一层完成,保证上层Node与下层自定义线程池及IOCP之间各自独立。
Node的异步I/O
- 事件循环
- 任务队列
事件循环
Node实现异步I/O的环节有事件循环、观察者和请求对象
- 事件循环:一次循环称为Tick,每个Tick的过程就是查看是否有事件待处理
- 观察者:每个Tick有一个或者多个观察者,判断是否有待处理事件的过程就是向观察者询问
- 请求对象:异步任务的回调不是开发者来调用,而是Node来调。请求对象是在JS发起调用到内核执行完成的过程中产生,这个对象包含了一次异步调用的上层回调、底层方法、执行结果等所有重要的状态。
- I/O线程池:我们知道单线程是Node的一个基本特点,单这仅仅对于JavaScript是单线程的的,Node的异步I/O其实是多线程的,不同的的异步操作会封装不同的请求对象然后在线程池中执行。
任务队列
除了异步I/O外还有一些非I/O的异步操作,我们知道任务可分为同步任务和异步任务,异步任务又有宏任务和微任务,不同的任务类型会有不同的队列异步不同的执行顺序
宏任务
- Timers 类型的宏任务队列
- setTimeout()
- setInterval()
- Check
- setImmediate()
- Close
- socket.on(‘close’, () => {})
- Poll
- 除了上面几种的其他所有回调
顺序
微任务
- process.nextTick()
- Promise.then()
process.nextTick()的优先级高于所有的微任务
在浏览器中是每执行
一个
宏任务都会清空一次微任务队列,Node中是执行了一种
宏任务之后去清空微任务
示例
setTimeout(() =>{
console.log('timer1')
Promise.resolve().then(() =>{
console.log('promise1')
})
}, 0)
setTimeout(() =>{
console.log('timer2')
Promise.resolve().then(() =>{
console.log('promise2')
})
}, 0)
// 浏览器 timer1 promise1 timer2 promise2
// node(v11前) timer1 timer2 promise1 promise2
在Node v11版本及之后上述Node的执行结果和浏览器是一致的,都是执行一个宏任务后去清空微任务队列
Node v11后的区别:浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的
需要额外注意的是setTimeout(()=>{},0)和setImmediate()同时在主线程上时,执行的顺序不一定,因为setTimeout的延时为0时,Node会把0设置为1ms,那么就有可能因为这1ms的延时而错过timer阶段先进入check阶段