Node笔记二(异步I/O和异步编程)

异步I/O

为什么要异步I/O
  1. 用户体验—Js和UI渲染公用一个线程,同步方式获取JS资源会使UI停顿;
  2. 资源分配—I/O与CPU计算是可以并行进行的,但是同步模式I/O会让后续任务等待,利用异步I/O,可以让单线程远离阻塞,更好的利用CPU.
    综上,异步I/O的目的是使I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余业务执行。

在这里插入图片描述

操作系统的异步I/O

异步/同步与阻塞/非阻塞是不同概念。
操作系统内核的I/O只有阻塞/非阻塞方式。阻塞I/O的特点是调用要等系统内核完成所有操作之后调用才结束,会使CPU等待I/O, 造成CPU资源的浪费。

内核在进行文件I/O操作时,通过文件描述符管理。应用程序若需要进行I/O调用,需要先打开文件描述符,然后根据文件描述符实现文件的读写。

小知识: 文件描述符是用于表述文件的引用抽象概念,形式上是一个非负整数,实际是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开现有的文件或者创建一个新的文件,内核向进程返回一个文件描述符。一般适用于nux系统,所有执行I/O操作的系统调用都会通过文件描述符。链接*

非阻塞I/O的轮询:
非阻塞I/O会立即返回当前调用状态,但是完整的I/O并未完成,需要重复调用I/O操作确认是否完成的方式叫做轮询。

轮询方式:(存在CPU判断I/O状态的CPU资源浪费)

  1. read: 最原始性能最低,重复调用检查I/O状态。
  2. select: 基于read方式的改进,通过对文件描述符上的事件状态进行判断。采用1024长度的数组存储状态,最多只能同时检查1024个文件描述符。
  3. poll:基于select改进。采用链表方式存储状态,避免数组长度限制。缺点是文件描述符较多使性能低下。
  4. epoll:Linux下效率最高的I/O事件通知机制。采用事件通知,执行回调的方式,在轮询时没有检查到I/O事件,会休眠,有事件发生则被唤醒。不会浪费CPU.
  5. kqueue.与epoll类似,但仅在FreeBSD系统下存在。

在这里插入图片描述

理想的非阻塞异步I/O:
虽然epoll方式效率较高,但在事件发生前的休眠期间CPU没有被利用,理想的非阻塞异步I/O是:应用程序发起非阻塞调用,可以直接处理下一个任务,I/O完成后通过信号或回调将数据传递给应用程序即可。
注:linux下原生有这样的异步I/O(AIO),但无法利用系统缓存。
在这里插入图片描述
现实的异步I/O
通过多线程方式模拟异步I/O——部分线程阻塞或非阻塞 + 轮询获取数据,一个线程进行计算处理,通过线程间的通信将I/O获取到的数据传递。

*nixwindows的异步I/O:
*nix平台下,Node起初用libeio(异步I/O库,采用线程池和阻塞I/O模拟异步I/O)配合libev实现异步I/O,Node v0.9.3 中自行实现线程池(libuv实现)完成异步I/O;
Windows下是基于IOCP实现异步I/O,原理也是线程池,只不过线程池由系统内核管理;

Node提供libuv抽象封装层,使所有平台兼容,保证上层的Node与下层的自定义线程池及IOCP独立。
(Node经典调用方式:JS调用Node核心模块,核心模块调用C/C++内建模块,内建模块通过libuv进行系统调用。)

注:Node中JS执行是单线程,但无论是Windows还是nix平台,内部完成I/O任务均有线程池实现。*

Node的异步I/O

要素:事件循环,请求对象,观察者,I/O线程池

  1. 事件循环(下一篇会单独讲)

    进程启动时,Node会创建类似While(true)循环,每次循环称为Tick,每个Tick过程会查看是否有事件待处理,有则取出事件及其相关回调并执行,然后进入下个循环;若没有事件则退出进程。

  2. 观察者

    异步I/O,网络请求均是事件的生产者,事件传递到对应的观察者(文件I/O观察者,网络I/O观察者等),事件循环从观察者那取出事件并处理。

  3. 请求对象

    JS发起调用到内核执行完I/O操作的过度过程中的中间物,所有状态将保存在该对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

  4. 回调

    线程池中的I/O调用完毕后,调用方法 (PostQueuedCompletionStatus()) 向IOCP(windows)提交执行状态,并将线程归还线程池。每次Tick,事件观察者会调用IOCP方法 (GetQueuedCompletionStatus()) 检查线程池是否有执行完的请求,若存在将请求对象加入I/O观察者的队列,将其作为事件处理。

非I/O异步API
  1. setTimeout

    setTimeout()或者setInterval()创建的定时器会被插入定时器观察者的红黑树中,每次Tick执行时取出,检查是否超过定时时间,超过则形成事件并立即执行回调。时间复杂度:o(lg(n))
    缺陷:不是很精确,具体看cpu时间片被调用的情况。

  2. process.nextTick()

    调用时会将回调函数放入队列中,下一轮Tick时取出执行。时间复杂度:o(1)

  3. setImmediate()

注:process.nextTick()-idle观察者,setImmediate()-check观察者,轮询检查中观察者优先级: idle > I/O > check

异步编程解决方案

异步编程中异常处理是个难点,因为try-catch只能捕获同步代码中的异常,对于异步中的无法得到想要的效果。

  • try-catch捕获同步:
    function sync() {
    	throw new Error('sync error');
    }
    
    try {
    	sync();
    } catch (err) {
    	console.log('error caught:', err.message);
    }
    
    // error caught: sync error
    
  • try-catch不能捕获异步:(要是能捕获到会打印出—catch error----)
    const asyncFunc = () => {
        setTimeout(() => {
            throw new Error('出错啦');
        });
    }
    try {
        asyncFunc();
    } catch (err) {
        //这里并不能捕获回调里面抛出的异常
        console.log("-----catch error------")
        console.log(err)
    }
    
    // throw new Error('出错啦');
        ^
    
    //Error: 出错啦
        //at Timeout.setTimeout [as _onTimeout] (/Users/yangyang/Projects/Exercises/Node/try-catch.js:3:15)
       // at ontimeout (timers.js:436:11)
       // at tryOnTimeout (timers.js:300:5)
       // at listOnTimeout (timers.js:263:5)
       // at Timer.processTimers (timers.js:223:10)
    

开始的解决方法——回调,error作为回调函数的第一个参数,也是node api回调参数方式,但是易造成回调地狱,对于错误的追踪也很麻烦:

const asyncFunc = (callback) => {
	setTimeout(function() {
		var rand = Math.random();
		if (rand < 0.5) {
			callback('callback 出错啦');
		} else {
			callback(null, rand);
		}
	}, 1000);
}

asyncFunc((err, result) => {
	if (err) {
        console.log("-----catch error------", err)
	} else {
		console.log('---success---', result);
	}
});

// 多异步串行的时候易出现回调地狱
asyncFunc(function(err, result) {
	if (err) {
		console.log('fail:', err);
	} else {
		console.log('success:', result);
		asyncFunc(function(err, result) {
			if (err) {
				console.log('fail:', err);
			} else {
				console.log('success:', result);
				asyncFunc(function(err, result) {
					if (err) {
						console.log('fail:', err);
					} else {
						console.log('success:', result);
					}
				});
			}
		});
	}
});

几种常见的解决方案:

  1. 事件发布/订阅

    const EventEmitter = require('events');
    
    class MyEmitter extends EventEmitter {}
    
    const myEmitter = new MyEmitter();
    
    myEmitter.on('error', (error) => {
      console.log('wow! it\'s an error');
    });
    myEmitter.emit('error', new Error('error'));
    
  2. promise

    const asyncFunc = () => new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('Promise 出错啦!');
        });
    });
    
    asyncFunc().then(function(result) {
    	console.log('success:', result);
    }, function(err) {
    	console.log('-----catch error------', err);
    });
    
  3. Iterator/Generator

    function *generator() {
      try {
        const x = (yield 'world')();
        return x;
      } catch (err) {
        console.error(err); // TypeError: (intermediate value) is not a function
      }
    };
    
    const it = generator();
    it.next();
    const res = it.next('bar');
    console.log(res); // { value: undefined, done: true }
    
  4. async/await

    const asyncFunc = () => new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('Async/await 出错啦!');
        });
    });
    
    async function f() {
        try {
            await asyncFunc();
        } catch (e) {
            console.log("-----catch error------")
            console.log(e)
        }
    }
    
    f()
    
  5. Domin(Node模块,已被弃用,不做例子说明)

Others:
异步编程解决方案:(涉及到手动实现promise)
https://juejin.im/post/5bc7d9bef265da0af879a293
深入理解JS异步编程:
https://nullcc.github.io/2018/11/04/深入理解Node.js异步编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值