本文力图详尽解释node的异步IO:
- 异步IO的产生背景
- Node中的异步IO具体的实现
- 非I/O的异步API
一.为什么要异步I/O?
Node面向网络而设计,Web应用现如今已不再是单台服务器可以胜任,在跨网络结构下,并发已是现代编程的标配。
以下从“用户体验”和“资源分配”两方面阐述异步I/O的必要性。
1. 用户体验
2. 资源分配
假若业务场景有一组互不相关的任务要完成,有两种方法:
- 单线程串行依次完成
- 多线程并行完成
串行执行的缺点:性能上,任意一个稍慢的任务都会导致后续执行代码被阻塞。本可以并行利用CPU等资源,但同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。
多线程并行的缺点: (1)创建线程和执行期线程上下文切换的开销较大;(2)多线程编程经常面临锁,状态同步等问题。
Node给出的方案:
利用单线程,远离多线程死锁,状态同步等问题;利用异步I/O让单线程原理阻塞,以更好的利用CPU等硬件资源。
二. Node底层是如何实现异步I/O的?
记住四个关键词:
- 事件循环
- 观察者
- 请求对象
- 线程池
先简洁直白的大致描述一下:
Node发出一个异步调用请求,然后继续执行业务代码;该请求被封装成一个"请求对象"并放入系统的线程池;待有可用线程完成该请求后,将异步结果放入该请求对象,并通知IOCP;每一次"事件循环"中,"观察者"尝试从IOCP中取出可用的"请求对象"并放入事件队列中, 并取出该对象中的回调函数和结果作为一个事件调用执行。
1. 事件循环
Node自身的执行模型----事件循环。
在进程启动时,Node便会创建一个类似while(true)的循环,每执行一次循环体的过程称为Tick。每个 Tick的过程就是查看是否有事件待处理,如有,就取出事件并执行这些相关的回调函数。然后进入下一个循环。如果不再有事件处理,就退出进程。
2. 观察者
正如上文提到,如何判断每个Tick过程中,是否有事件需要处理呢?这就引出了”观察者“。
事件循环是一个典型的“生产者/消费者”模型。异步I/O,网络请求等则是事件的生产者,源源不断的为Node提供不同类型的事件,这些事件被传递到对应的观察者那里。事件循环则从观察者那里取出事件并处理。
在windows下,这个循环基于IOCP创建,而在*nix下则基于多进程创建。
3. 请求对象
- 以fs.open()为例,javascript调用node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用。
- 系统调用中,对应的是uv_fs_open()方法,该方法创建了一个FSReqWrap请求对象。该对象封装了从javascript层传入的参数,当前方法和回调函数等所有的状态。
- 系统层将该请求对象放入线程池等待执行。
- (此时,javascript调用立即返回,即javascript层发起的异步调用的第一阶段到此结束,javascript线程可以继续执行当前任务的后续操作)
4. 执行回调
以上提到,异步I/O的第一阶段:组装请求对象,放入线程池等待执行;
第二阶段:回调通知。
-
线程池中的I/O操作完成后,会将结果挂载在请求对象上,然后通过PostQuenedCopletedStatus()通知系统层的IOCP,告知当前的操作对象已经操作完成。
-
Node层在每一次Tick的执行中,会调用IOCP的GetQuenedCompletedStatus()方法检查是否有执行完的请求,如果存在则将该请求对象加入事件队列中,并取出结果和回调函数调用执行。
从以上可看出:Windows下主要是通过IOCP来向系统内核发送已完成的I/O调用和从系统内核中取出已完成的I/O操作,配以事件循环,以此完成异步I/O的操作。
三. 非I/O的异步API
- setTimeout()
- setInterval()
- setImmediate()
- process.nextTick()
1. 定时器
调用setTimeout()或setInterval()创建的定时器会被插入到定时器内部的一个红黑树上。每次Tick执行时,会从该红黑树上迭代取出定时器对象,检查是否超过规定时间,如果超过就形成一个事件,并立即执行其回调函数。
2. process.nextTick()
每次调用process.nextTick()会将回调函数放入一个队列数组中,下一次Tick时取出该数组中全部的回调函数并执行。(时间负责度O(1))
3. setImmediate()
setImmediate()类似于process.nextTick(),都是将回调函数延迟执行。
区别在于:
- 事件循环对于观察者是有先后顺序的。setImmediate()对应的观察者的优先级低于process.nextTick()对应的观察者。
- process.nextTick()的回调函数保存在数组中,setImmediate()的回调函数保存在链表中。行为上,processTick()在每轮循环中会将数组中的回调函数全部取出并执行,而setImmediate()在每轮循环中执行链表中的一个回到函数。
佐证代码:
process.nextTick(funciton() {
console.log('nextTick延迟执行1');
})
process.nextTick(funciton() {
console.log('nextTick延迟执行2');
})
setImmediate(funciton() {
console.log('setImmediate延迟执行1');
process.nextTick(function() {
console.log('强势插入');
})
})
setImmediate(fucntion() {
console.log('setImmediate延迟执行2');
})
console.log('正常执行');
//正常执行
//nextTick延迟执行1
//nextTick延迟执行2
//setImmediate延迟执行1
//强势插入
//setImmediate延迟执行2
四. 总结
事件驱动的本质:通过主循环加事件触发的方式来运行程序。
注:以上均是自己技术栈的整理,仅供备忘。如需交流:stevekeol(微信号)
网友评论