异步IO
一、为什么要使用异步I/O
用户体验
javascript在单线程上执行,它与UI线程是一个线程,如果使用同步,当javascript在执行的时候UI渲染就必须要停止等待,这样就是的用户的体验极差。
如果网页需要请求一些资源,通过同步的方式获取的话,那么我们就必须要等待js从服务器完全获取到资源后再继续执行,这期间UI等待,就会使得与用户的交互极差,影响用户的体验感。
// 现在请求两个资源
//耗时为M毫秒
getData('from_db');
//耗时为N毫秒
getData('from_remote_api');
如果是同步,需要耗时(M + N)
;
如果是异步,需要耗时Max(M, N)
;
随着应用的复杂性,情景会变成M+N+...和Max(M,N,...)
,此时同步和异步的优劣就会更加凸显。另一方面,随着网站和应用的扩展,数据往往会分布到多台服务器上,而分布意味着M和N的值会线性增长,这也会放大异步和同步在性能上的差异。总之,IO是昂贵的,分布式IO是更昂贵的!
资源分配
单线程同步IO
会因阻塞IO使得硬件资源无法得到更优的利用。
多线程编程
优点: 可以利用多核CPU有效提升CPU的利用率
缺点: 编程中的死锁、状态同步使得程序员很是头疼。
node的异步IO
node采用的异步IO,利用单线程,远离了多线程死锁、状态同步,利用异步让单线程远离了阻塞,使得CPU得到更好的利用。
为了弥补单线程无法利用多核CPU的问题,Node提供了子进程 `childProcess` ,将一些运算多的任务放入子进程进行高效的运算。
二、阻塞I/O 和 非阻塞 I/O
阻塞IO
阻塞的IO操作就是发起IO操作后,线程阻塞等待IO完成,这期间cpu得不到有效利用。
非阻塞IO
非阻塞IO操作其实就是发起IO操作后,通过事件轮巡,或者事件通知机制,不断查询IO操作是否完成,或者是主线程进入休眠等待事件通知IO结束,然后继续向下执行代码,实际上非阻塞IO期间,cpu要不用来查询要不用来休眠,也没有得到有效利用。依旧是同步IO。
三、node的异步I/O
完成整个异步IO需要单个环节 事件循环
观察者
请求对象
。
实际上node的异步IO是采用了线程池技术,发起异步IO时,把io操作扔到线程池里面执行,然后主线程继续执行其他操作,io执行完毕通过线程间通信通知主线程,主线程执行回调。
IO线程是由Libuv
(Linux
下由libeio
具体实现;window
下则由IOCP
具体实现)管理的线程池控制的,本质上是多线程。即采用了线程池
与阻塞IO
模拟了异步IO
。
异步IO原理
当遇到IO时,将其放入线程池中的一个IO线程,让该任务在IO线程上执行,在IO线程上是阻塞IO方式执行的,然后在主线程上继续执行,当遇到另一个IO任务时,将其在放入线程池,然后在另一条IO线程上执行(同样是阻塞IO方式),主线程继续执行着。
1、事件循环
正式因为事件循环使得回调函数非常的普遍。
1. node进程启动的时候,会创建一个类似的 while(1) 循环,每执行依次循环的过程被称为是 `Tick`。
2. Tick过程就是查看当下是否有待处理的事件,如果有则取出相关事件以及回调函数,然后执行回调函数(在主线程中执行)。
3. 然后进入下一个循环,继续检测如果不再有事件处理,就推出。
当I/O线程上的任务(阻塞I/O)执行完毕之后,就会产生一个事件,这就是事件循环中的事件的产生由来。
2. 观察者
Node中事件的主要来源是网络请求和文件IO等。这些事件对应的观察者就是网络I/0观察者、文件I/0观察者。
3. 请求对象
在从js发起调用起到内核执行I/O完毕之后。这个中间过程有一种中间产物,称为是请求对象
。
- 以打开文件为例子
0. 异步调用任务
1. js调用核心模块
2. 核心模块调用C++内建模块
3. 内建模块在`libuv`层,分平台处理。实质上调用的都是`uv_fs_open`方法。
4. 在调用的过程中,创建一个`FSReqWrap`请求对象。【这就是我们的主角请求对象了】
5. 对象创建完毕后,设置好参数和回调函数,就会将其推入线程池中等待执行了。
6. js线程继续执行后续的任务,当前的IO操作在线程池中执行,不管IO线程上是阻塞还是非阻塞,都不会影响主线程的执行,因此这就达到了异步的目的了。
- 到这里其实就完成了异步IO的第一步了,
回调通知
则是第二步。
4. 执行回调
当IO线程中的任务执行完毕后,就会将执行结果放在请求对象中。然后通知TOCP
。TOCP
检查任务是否完成。如果完成了就将I/O请求对象加入观察者队列
中,当作事件处理
。然后通过事件循环来执行回调函数。
- 注意: Windows下是
TOCP
,Linux下是通过epoll
。
四、 node的非I/O的异步API
1. 定时器
定时器的实现原理同异步IO,只是没有使用线程池。
setTimeout()
每次Tick,都会从该红黑树迭代取出定时器对象,然后检查是否超过了时间,如果超时那么就形成一个事件,回调函数执行。
2. process.nextTick()
立即执行一个异步任务
我们之前可能都这么做
setTimeout(function(){
//
}, 0);
使用上面的方法浪费性能,使用process.nextTick()则更为轻量。
process.nextTick(function(){
//
})
区别
// 原始
A();
B();
C();
A();
process.nextTick(B);
C();
A();
setImmediate(B);//或者setTimeout(B,0);
C();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。