异步I/O
为什么要异步I/O
- 用户体验—Js和UI渲染公用一个线程,同步方式获取JS资源会使UI停顿;
- 资源分配—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资源浪费)
- read: 最原始性能最低,重复调用检查I/O状态。
- select: 基于read方式的改进,通过对文件描述符上的事件状态进行判断。采用1024长度的数组存储状态,最多只能同时检查1024个文件描述符。
- poll:基于select改进。采用链表方式存储状态,避免数组长度限制。缺点是文件描述符较多使性能低下。
- epoll:Linux下效率最高的I/O事件通知机制。采用事件通知,执行回调的方式,在轮询时没有检查到I/O事件,会休眠,有事件发生则被唤醒。不会浪费CPU.
- kqueue.与epoll类似,但仅在FreeBSD系统下存在。
理想的非阻塞异步I/O:
虽然epoll方式效率较高,但在事件发生前的休眠期间CPU没有被利用,理想的非阻塞异步I/O是:应用程序发起非阻塞调用,可以直接处理下一个任务,I/O完成后通过信号或回调将数据传递给应用程序即可。
注:linux下原生有这样的异步I/O(AIO),但无法利用系统缓存。
现实的异步I/O
通过多线程方式模拟异步I/O——部分线程阻塞或非阻塞 + 轮询获取数据,一个线程进行计算处理,通过线程间的通信将I/O获取到的数据传递。
*nix
和windows
的异步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线程池
- 事件循环(下一篇会单独讲)
进程启动时,Node会创建类似While(true)循环,每次循环称为Tick,每个Tick过程会查看是否有事件待处理,有则取出事件及其相关回调并执行,然后进入下个循环;若没有事件则退出进程。
- 观察者
异步I/O,网络请求均是事件的生产者,事件传递到对应的观察者(文件I/O观察者,网络I/O观察者等),事件循环从观察者那取出事件并处理。
- 请求对象
JS发起调用到内核执行完I/O操作的过度过程中的中间物,所有状态将保存在该对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。
- 回调
线程池中的I/O调用完毕后,调用方法 (
PostQueuedCompletionStatus()
) 向IOCP(windows)提交执行状态,并将线程归还线程池。每次Tick,事件观察者会调用IOCP方法 (GetQueuedCompletionStatus()
) 检查线程池是否有执行完的请求,若存在将请求对象加入I/O观察者的队列,将其作为事件处理。
非I/O异步API
- setTimeout
setTimeout()或者setInterval()创建的定时器会被插入定时器观察者的红黑树中,每次Tick执行时取出,检查是否超过定时时间,超过则形成事件并立即执行回调。时间复杂度:o(lg(n))
缺陷:不是很精确,具体看cpu时间片被调用的情况。 - process.nextTick()
调用时会将回调函数放入队列中,下一轮Tick时取出执行。时间复杂度:o(1)
- 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);
}
});
}
});
}
});
几种常见的解决方案:
-
事件发布/订阅
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'));
-
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); });
-
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 }
-
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()
-
Domin(Node模块,已被弃用,不做例子说明)
Others:
异步编程解决方案:(涉及到手动实现promise)
https://juejin.im/post/5bc7d9bef265da0af879a293
深入理解JS异步编程:
https://nullcc.github.io/2018/11/04/深入理解Node.js异步编程