4

异步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线程中的任务执行完毕后,就会将执行结果放在请求对象中。然后通知TOCPTOCP检查任务是否完成。如果完成了就将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();


Meils
1.6k 声望157 粉丝

前端开发实践者