浅谈NodeJs异步I/O-4
发布于 6 年前 作者 halu886 2919 次浏览 来自 分享

该文章阅读需要5分钟,更多文章请点击本人博客halu886

Node除去异步I/O,还有一些与I/O无关的异步API需要特别关注:setTimeout(),setInterval(),setImmediate(),process.nextTick()。

定时器

setTimeOut()setInterval()和浏览器的API的功能一致的,分别为单次或多次执行任务。实现原理和异步I/O相似。但是不需要I/O线程池参与,定时器创建后会插入定时器红黑树中。在每一次的Tick中,定时器观察者会从红黑树迭代取出一个定时器,判断是否超时,如果超时会执行这个定时器的回调。

如图所示,两者的行为是一致的,不过setInterval()是重复的。

定时器的问题在于不精确。例如,一个定时器任务是10ms后执行,但是当9毫秒时,有个任务占用了CPU时间片5毫秒,那么当14毫秒时才能执行定时器任务,此时已经逾期了4ms。

1

precess.nextTick()

当未了解process.nextTick()时,为了立即执行一个异步任务,会这样实现

setTimeout(function(){
    //TODO
},0);

由于定时器的特点,不够精确。并且,创建和迭代一个定时器,需要动用红黑树,这样时比较浪费性能的。但是process.nextTice()是比较轻量级的。

process.nextTick = function(callback){
    if(process._exiting) return;

    if(tickDepth >= process.maxTickDepth)
        maxTickWarn();
    
    var tock = { callback: callback};
    if(process.domain) tock.domain = process.domain;
    nextTickQueue.push(tock);
    if(nextTickQueue.length){
        process._needTickCallback();
    }
}

调用process.nextTick(),只会将回调函数放入队列中,下一轮Tick取出。定时器中的红黑树的时间复杂度为O(lg(n)),nextTick()的时间复杂度为O(1)。所以process.nextTick()更加高效。

setImmediate()

setImmediate()process.nextTick()功能相似,都是将回调延期执行。但是在Nodev0.9.0之前,setImmediate()还未实现,所以相关功能主要是通过process.nextTick()
例如:

process.nextTick(function(){
    console.log('延期执行');
})
console.log('正常执行');
//正常执行
//延期执行  

而用setImmedieate()实现时,相关代码如下:

setImmediate(function(){
    console.log('延期执行');
})
console.log('正常执行');
//正常执行
//延期执行

其实它们还是有细微差别的,如果将它们放在一起会怎么样呢?

process.nextTick(function(){
    console.log('nextTick延迟执行');
})
setImmediate(function(){
    console.log('setImmediate延迟执行');
})
console.log('正常执行');
// 正常执行
// nextTick延迟执行
// setImmediate延迟执行

从结果上来看,nextTick的优先级高于setImmediate,这是因为事件循环对观察者是有先后顺序的,process.nextTick()是idle观察者,setImmediate()是check观察者。idle观察者优先级高于I/O观察者,I/O观察者优先级高于check观察者。

具体实现上,process.nextTick()会将回调函数放入一个数组中,而setImmediate()则会将回调放入一个链表中。在每轮循环中,nextTick会将数组中的所有循环全都执行完,而setImmediate则会执行链表中一个回调函数。

process.nextTick(function(){
    console.log('nextTick延迟执行1');
});

process.nextTick(function(){
    console.log('nextTick延迟执行2');
})

setImmediate(function(){
    console.log('setImmediate延迟执行1');
    process.nextTick(function(){
        console.log('强势插入');
    });
})

setImmediate(function(){
    console.log('setImmediate延迟执行2');
})
console.log('正常执行');
//正常执行
//nextTick延迟执行1
//nextTick延迟执行2
//setImmediate延迟执行1
//强势插入
//setImmediate延迟执行2

从执行结果上看,当执行完第一个setImmediate后不是立即执行第二个setImmediate,而是开始下一个循环,按优先级执行process.nextTick()的回调再执行setImmediate()的回调。之所以这样设计是为了每轮循环尽快的结束任务,防止阻塞后续的I/O调用

事件驱动与高性能服务器

尽管我们前面使用fs.open()作为异步I/O的例子,但是在Node中网络套接字的读取也应用到了异步I/O,网络套接字的监听到请求都会形成事件交给I/O观察者。事件循环不停的处理网络I/O事件,如果Javascript有传入回调函数,这些事件最后会最终传递业务逻辑层进行处理。
2

以下是比较经典的服务器模型,下面对比一下他们优缺点:

  1. 同步式,一次只能处理一个请求,其余时候都处于等待状态。
  2. 每进程/每请求,为每个请求启动一个进程,虽然可以处理多个请求,但是不易拓展,比较系统资源有限。
  3. 每线程/每请求,每个请求启动一个线程,虽然线程比进程轻量,但是当高并发请求访问时,也很容易把内存吃光导致服务器缓慢,每线程/每请求比每线程/每请求拓展性更好,但对于大型站点而言依然不够。

每线程/每请求的方式依旧被Apache采用。Node通过事件循环处理请求,省略了创建线程和销毁线程的开销,通过操作系统在调度任务时线程比较少,上下文切换代价比较低。这使服务器能够有条不紊的处理请求,即使在大量连接的情况下,也不受线程上下文切换的影响,这是Node高性能的一个原因。

事件驱动的高效已经逐渐被业内重视。例如Nginx,摒弃了多线程的方式,采用了和Node相同的事件驱动。并且如今Nginx大有取代Apache的趋势。Node具有和Nginx相同的特性,不过Nginx采用纯C开发的,新能极高,但是它仅适用于作为Web服务器,用于反向代理或均衡负载等服务,在处理业务方面上较为欠缺。Node则是一个高性能的平台,可以利用它构建与Nginx相同的功能,也可以处理其他业务,两者相比,Node没有Nignx在Web服务器上那么专业,但场景更大,自身性能也不错。

以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)

回到顶部