异步I/O
1. 推行异步的原因
- 在浏览器中,JavaScript在单线程中执行,且与UI渲染公用一个线程,这就会出现JavaScript在执行时,UI渲染和响应处于停滞状态的现象;
- 如果脚本执行时间超过100ms,用户就会感觉到页面卡顿,而在BS模型中,网速的限制会给网页的体验造成很大干扰。
- 因此,前端采用异步的方式可以解决UI渲染阻塞的问题,但响应也却决于服务器的速度。
- **同样的任务t1~tn,同步的执行时间T=t1+t2+…+tn,而异步的执行时间为Max(t1,t2,…,tn)。**因此,随着业务的增大,异步的优势会逐渐凸显出来。
- 多线程的代价在于创建线程和执行期线程切换上下文造成的开销比较大,且在一些复杂的场景中会经常面临死锁、状态同步等问题;但在多核CPU上能够有效提升CPU的利用率;
- 单线程同步编程模型会因为阻塞I/O而导致硬件资源无法得到更优化的使用。为了弥补单线程无法利用多核CPU的问题,Node推出了Webworkers的子进程概念,子进程可以通过工作进程高效地利用CPU和I/O。
2. 异步I/O和非阻塞I/O
- 从计算机内核I/O而言,同步与异步、阻塞和非阻塞是两回事。
2.1 阻塞I/O
- 操作系统内核的I/O只有阻塞和非阻塞之分。
- 调用阻塞I/O后,应用程序需要等待I/O完成后才返回结果。
- 阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后才调用结束。
- 阻塞I/O造成CPU等待,浪费了等待时间,CPU无法得到充分利用。因此,内核提供了非阻塞I/O来解决这个问题。
2.2 非阻塞I/O
- 非阻塞I/O与阻塞I/O的区别在于,前者调用之后会立即返回。
- 返回之后,CPU的时间片可以用来处理其他的事物,提升性能明显。
- 但是,由于非阻塞I/O没有完整地完成,立即返回的是当前的调用状态。应用程序需要重复调用I/O来确定是否已经完成,这种技术称为轮询。
2.3 轮询
-
轮询主要有以下几种:
read:最原始、性能最低的一种。通过重复调用来检查I/O的状态来完成完整数据的读取。在得到最终数据前,CPU一直耗用在等待上。
select:通过对文件描述符上的事件状态来进行判断。但它采用一个1024长度的数组来存储状态,因此它最多可以同时检测1024个文件描述符。
poll:在select基础上,解除了数组的限制,换成链表的形式。当文件描述符过多,性能会下降。
epoll:进入轮询的时候,如果没有检测到I/O事件,将进入休眠,直到事件将他唤醒。它利用了事件通知、执行回调的方式,不是通过遍历查询,因此不会浪费CPU资源。
-
轮询技术满足了非阻塞I/O确保获取完整数据的需求,但是它也是一种同步方式,仍需要让应用程序等待I/O完全返回,依旧花费了很多时间等待。
2.4 Node的异步I/O
2.4.1 事件循环
- 在进程启动时,Node会创建一个循环,每执行一次循环称为Tick。
- 每次Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数;如果存在相关的回调函数,则执行它们,再进入下一个循环,直到没有待处理事件,直接退出。
2.4.2 观察者
- 每个事件循环有一个或多个观察者,判断是否有事件的过程就是向观察者进行询问。
- 在浏览器中,事件来自于用户点击或者加载某些文件时产生,这些事件都拥有观察者;
- 在Node中,事件来源于网络请求、文件I/O等,这些事件拥有对应的网络I/O观察者,文件I/O观察者等。
- 在Windows下,这个循环基于IOCP;在*nix下基于多线程创建。
2.4.3 请求对象
- 从JavaScript发起调用到内核执行完I/O操作的过程中,存在一种中间产物——请求对象。
- 从JavaScript调用Node核心模块,核心模块会调用C++内建模块,内建模块通过libuv进行系统调用。
- 在系统调用的过程中,会创建一个请求对象, JavaScript层传入的参数和当前的方法都会被封装在这个对象中