您的位置:首页 > Web前端 > Node.js

深入浅出node第3章异步I/O摘录

2018-03-08 13:31 302 查看

3 异步I/O

3.1 为什么要异步I/O

3.1.1 用户体验

//消费时间为m
getData('from_db')
//消费时间为n
getData('from_remote_api')


getData('from_db',function(re){
//消费时间m
});
getData('from_remote_api',function(re){
//消费时间n
});


对比两者的时间总消耗,前者为m+n,后者为max(m,n)

I/O是昂贵的,分布式I/O更昂贵

3.1.2 资源分配

假设业务场景中有一组互不相关的任务需要完成,现行的主流方法有以下两种:

单线程串行

多线程并行

单线程串行的缺点在于性能,任意一个慢的任务都会导致后续执行代码被阻塞。在计算机资源中,通常I/O与CPU计算之间是可以并行进行的。但是同步的编程模型导致的问题是,I/O的进行会让后续任务等待,造成资源不能很好利用。

多线程的代价在于创建线程和执行线程上下文切换的开销较大。在复杂的业务中,多线程编程经常面临锁、状态同步等问题。

单线程同步编程模型会导致因阻塞I/O导致硬件资源得不到充分利用。多线程编程模型也会因为编程中的死锁、状态同步等问题让开发人员头疼。

Node在两者之间给出了方案:利用单线程,远离死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,更好地使用CPU。

为了米波单线程无法使用多核CPU的缺点,Node提供了类似前端浏览器中的Web Worker的子进程。



3.2 异步I/O现状

Brendan Eich引援18世纪英国文学家约翰逊所说,‘他的优秀之处并非原创,他的原创之处并不优秀’,以之评价自己创造的js。

3.2.1 异步I/O与非阻塞I/O

从计算机内核I/O而言,异步/同步 和 阻塞/非阻塞 是两回事。

操作系统对I/O只用两种方式:阻塞、非阻塞。





操作系统对计算机进行了抽象,将所有输入输出设备抽象为文件。内核在进行文件I/O操作时,通过文件描述符进行管理。应用程序如果需要进行I/O调用,需要先打开文件描述符,然后再根据文件描述符去实现文件的读写。

阻塞I/O完成整个获取数据的过程,而非阻塞I/O则不带数据直接返回,要获取数据,还需要通过文件描述符再次进行读取。

非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,性能提升明显。但是应用程序为了获取完整数据,需要重复调用I/O操作来确认是否完成,这种重复调用判断操作是否完成的技术叫做轮询。

epoll。linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检测到I/O事件,将会进行休眠,知道事件发生唤醒他。他是真实利用了事件通知、执行回调的方式,而不是遍历查询所以不会浪费CPU,示意图如下:



3.2.2 理想的非阻塞异步I/O



3.2.3 现实的异步I/O



3.3 Node的异步I/O

3.3.1 事件循环

进程启动时,node会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称之为Tick。每个tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行他们。然后进入下一个循环,

3.3.2 观察者

在每个tick的过程中,如何判断是否有事件需要处理呢?观察者。

每个事件循环中都有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为node提供事件,这些事件被传递到对应的观察者那里,时间循环从观察者那里取出事件并处理。

3.3.3 请求对象

请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象上,包括送入线程池等待执行以及I/O操作完毕后的回调函数。

3.3.4 执行回调



事件循环、观察者、请求对象、I/O线程池共同构成了node的异步I/O模型的基本要素。

3.4 非I/O的异步API

3.4.1 定时器

定时器的问题在于,他并非精确的(在容忍范围内);尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,他也许已经超时很久了。比如通过setTimeout设定的一个任务在10毫秒之后执行,但是在9毫秒后,一个任务占用了5毫秒的cpu时间片,再次轮到定时器执行时,时间就已经过期4毫秒。

3.4.2 process.nextTick()

每次调用process.nextTick方法,只会将回调函数放入队列中,在下一轮tick时取出执行。定时器采用红黑树的操作时间复杂度为O(lg(n)),nextTick的时间复杂度为O(1)。process.nextTick更加高效

3.4.3 setImmediate

在node v0.9.1之前,setImmediate还没实现,那个时候实现类似的功能主要是通过process.nextTick来完成。

process.nextTick中回调函数的优先级高于setImmediate。原因在于事件循环中对观察者的检查是有先后顺序的,process.nextTIck属于idle观察者,setImmediate属于check观察者。在每一轮循环中,idle观察者优先于I/O观察者,I/O观察者优先于check观察者。

在具体实现上,process.nextTick的回调函数保存在一个数组中,setImmediate的结果保存在链表中。

//加入2个nextTick的回调函数
process.nextTick(() => {
console.log(`nextTick延迟执行1`);
});
process.nextTick(()=>{
console.log(`nextTick延迟执行2`);
});
//加入两个setImmediate的回调函数
setImmediate(()=>{
console.log(`setImmediate延迟执行1`);
//进入下次循环
process.nextTick(()=>{
console.log(`强势插入`);
});
});
setImmediate(()=>{
console.log(`setImmediate延迟执行2`);
});
setTimeout(()=>{
console.log(`timeout 1`);
process.nextTick(()=>{
console.log(`timeout tick`);
});
},0);
setTimeout(()=>{
console.log(`timeout 2`);
},0);


我测试的输出是:

nextTick延迟执行1
nextTick延迟执行2
timeout 1
timeout 2
timeout tick
setImmediate延迟执行1
setImmediate延迟执行2
强势插入
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  node.js