异步IO

521 阅读4分钟

异步IO

Node基调=异步IO(事件循环+IO线程池)+单线程+事件驱动

一.为什么要异步IO

Node面向网络设计(跨网络),导致并发是标配。具体从用户体验资源分配展开:

1. 用户体验

前端浏览器 JS执行UI渲染共用一个线程,因此JS的阻塞(同步请求)会导致UI渲染阻塞(用户交互体验差)。
前端通过异步请求解决这个问题。
在后端,由于数据分布式已成常态,因此异步IO可以有效地提高资源请求速度,也因此可以更快的响应前端请求,提高用户体验。

2. 资源分配

  • 多线程能够提高CPU使用率,但会带来死锁 状态同步 线程创建和切换开销等问题。
  • 同步IO会阻塞硬件资源。

基于以上两点,单线程异步IO成了最优解(通过子进程提高CPU使用率)。

单线程异步IO

二.异步IO实现现状

1. 异步IO与非阻塞IO

异步IO看起来等同于非阻塞IO,但从计算机系统内核的角度而言,只认阻塞/非阻塞IO,将其提供给应用程序(调用方)。

  • 阻塞IO特点是++一定需要在系统内核层面完成所有操作,调用才完成++ 阻塞IO

  • 非阻塞IO特点是 ++不带数据直接返回,但想要获取数据,还得轮询++

非阻塞IO

2. 理想的非阻塞异步IO

...

3. 现实异步IO

要实现理想的异步IO,需要使用线程池来模拟异步IO。

线程池模拟异步I/O

三.Node的异步IO

Node异步IO的实现=事件循环(Node的执行模型)+观察者+请求对象

1. 事件循环

Node进程开启后,会执行一个类似while(true)的循环(Tick),循环体是:

  • Node进程开始,检查是否有事件待处理
    • 有事件:取出事件,检查是否有关联回调
      • 有回调:执行回调
      • 无回调:本轮循环结束,开始下一循环
    • 无事件:结束Node进程

Tick流程图

2. 观察者

  • Node进程通过观察者来判断是否有事件需要处理:Node在每轮Tick,都会询问观察者是否有事件待处理。
  • 观察者可以有多个
  • 一个观察者可以对应多个事件,++观察者将事件分类++。

3. 请求对象

请求对象JS执行异步IO调用到回调函数执行 这一过程中重要的中间产物

异步IO第一阶段

  • 发起异步IO调用
    • JS调用fs核心模块
    • 核心模块调用C++内建模块
    • libuv进行平台判断
    • 调用uv_fs_open()
  • uv_fs_open函数中包装请求对象
    JS调用时传入的参数回调都会被作为属性添加到请求对象FSReqWrap中。
  • 将请求对象推入线程池

发起异步IO调用

4. 执行回调

异步IO第二阶段

  • IO操作执行完成后,将结果添加到请求对象result属性,并丢给IO观察者
  • 事件循环通过IO观察者获取到请求对象,作为事件处理。

5. 小结

异步IO流程

四.非IO的异步API

1. 定时器

JS调用的定时器将被插入到定时器观察者的红黑树中,每次Tick都会检查下是否超过时间,如果超过,就形成一个事件,回调函数立即执行。

setTimeout

2. process.nextTick()

相较于创建定时器对象process.nextTick()更加轻量,它只会将++回调放入队列++,在下一次Tick时取出执行。

3. setImmediate()

setImmediate()效果看起来与process.nextTick()一样。两者区别:

  • 优先级不同:setImmediate()属于check观察者process.nextTick()属于idle观察者,事件循环对于两种观察者的检查优先级不同:++idle观察者>IO观察者>check观察者++
  • 实现不同:process.nextTick()将回调保存在一个数组,setImmediate()将回调保存在链表中。每轮Tick,数组中的回调会全部执行完,而链表中的回调每轮只会执行一个。
// 加入两个nextTick()的回调函数
process.nextTick(function () {
  console.log('nextTick延迟执行1');
});
process.nextTick(function () {
  console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
  console.log('setImmediate延迟执行1');
  // 进入下次循环
  process.nextTick(function () {
    console.log('强势插入');
  });
});
setImmediate(function () {
  console.log('setImmediate延迟执行2');
});
console.log('正常执行');
正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
强势插入
setImmediate延迟执行2

五.事件驱动与高性能服务器

事件驱动的实质是++主循环+事件触发++

Node构建web服务器

IO观察者的事件回调,理解为接收请求这一事件的回调

六.总结

事件循环是异步实现的核心,与浏览器端的执行模型基本保持一致。