I/O模型、Libuv和Eventloop

1、I/O模型

①常见的IO模型:Linux(UNIX)操做系统中的网络IO模型为例node

  1. Blocking I/O 同步阻塞IO
  2. Non-blocking I/O 同步非阻塞IO
  3. I/O Multiplexing IO多路复用
  4. Signal-blocking I/O 信号驱动IO
  5. Asynchronous I/O 异步IO

②基本概念的定义:linux

IO 指的是输入输出,一般指数据在内部存储器和外部存储器或其余周边设备之间的输入和输出。简而言之,从硬盘中读写数据或者从网络上收发数据,都属于IO行为。编程

  • IO:内存IO、网络IO和磁盘IO,一般咱们说的IO指的是后二者。
  • 阻塞和非阻塞:在调用结果在返回以前,当前线程是否挂起,即发起IO请求是否会被阻塞。
  • 同步和异步:若是作阻塞I/O调用,应用程序等待调用的完成的过程就是一种同步情况。相反,I/O为非阻塞模式时,应用程序则是异步的。

③完成一次IO的过程: 以读一个文件为例,一个IO读过程是文件数据从磁盘→内核缓冲区→用户内存的过程。segmentfault

同步与异步的区别主要在于数据从内核缓冲区→用户内存这个过程需不须要用户(应用)进程等待,即实际的IO读写是否阻塞请求进程。(网络IO可把磁盘换作网卡)windows


一、同步阻塞IO

阻塞 I/O是最简单的 I/O 模型,通常表现为进程或线程等待某个条件,若是条件不知足,则一直等下去。条件知足,则进行下一步操做。promise

应用进程经过系统调用 recvfrom 接收数据,但因为内核还未准备好数据报,应用进程就会阻塞住,直到内核准备好数据报,recvfrom 完成数据报复制工做,应用进程才能结束阻塞状态。浏览器


二、同步非阻塞IO

应用进程经过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。若是没有准备好,内核会返回 error ,应用进程在获得 error 后,过一段时间再发送 recvfrom 请求。若是某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。在发送请求的时间间隔中,进程能够先作别的事情。安全


三、IO多路复用

IO多路复用是多了一个select函数,多个进程的IO能够注册到同一个select上,当用户进程调用该selectselect会监听全部注册好的IO,若是全部被监听的IO须要的数据都没有准备好时,select调用进程会阻塞。当任意一个IO所需的数据准备好以后,select调用就会返回,而后进程在经过recvfrom来进行数据拷贝。bash

这里的IO复用模型,并无向内核注册信号处理函数,因此,他并非非阻塞的。进程在发出select后,要等到select监听的全部IO操做中至少有一个须要的数据准备好,才会有返回,而且也须要再次发送请求去进行文件的拷贝。服务器


四、信号驱动IO

应用进程预先向内核注册一个信号处理函数,而后用户进程返回,而且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中。


五、异步IO

应用进程发起aio_read操做以后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操做完成时,如何通知进程,而后就马上去作其余事情了。当内核收到aio_read后,会马上返回,而后内核开始等待数据准备,数据准备好之后,直接把数据拷贝到用户控件,而后再通知进程本次IO已经完成。


六、五种IO模型对比

阻塞IO模型、非阻塞IO模型、IO多路复用和信号驱动IO模型都是同步的IO模型,由于不管以上那种模型,真正的数据拷贝过程,都是同步进行的。


2、Libuv

libuv是一个高性能事件驱动库,屏蔽了各类操做系统的差别从而提供了统一的API。libuv严格使用异步、事件驱动的编程风格。其核心工做是提供事件循环及 基于I/O 或其余活动事件的回调机制。libuv库包含了诸如计时器、非阻塞网络支持、异步文件系统访问、线程建立、子进程等核心工具。

一、 句柄和请求

libuv给用户提供了两种方式与event loop一块儿协同工做,一个是句柄(handle)一个是请求(request)。

句柄(handle)表明了一个长期存在的对象,这些对象当处于活跃状态的时候可以执行特定的操做。例如:一个准备(prepare)句柄在活跃的时候能够在每一个循环中调用它的回调一次。一个TCP服务器的句柄在每次有新的链接的时候都会调用它的链接回调函数。

请求(request)通常表明短时操做。这些操做能用做用于句柄之上。写请求用于在句柄上写数据;还有一些例外,好比说getaddrinfo请求不须要句柄而是直接在循环中执行。

二、 I/O循环

I/O循环或者叫作事件循环是整个libuv的核心部分。I/O循环创建了全部IO操做的执行环境,I/O循环会被绑定在一个线程之上。咱们能够运行多个事件循环,只要每个都运行在不一样的线程之上。libuv事件循环不是线程安全的,因此全部包含事件循环的API及句柄都不是线程安全的。

事件循环遵循最广泛的单线程异步I/O方法:全部I/O或者网络操做在非阻塞的socket上执行,这个socket会使用基于平台的组好的poll机制:在linux上使用epoll,在OSX和其余BSD平台上使用kqueue,在sunOS上使用event ports,在windows上使用IOCP。做为循环迭代的一部分,循环会阻塞以等待socket上的I/O活动,这些活动已经被加到socket的触发实践中了,一旦这些条件知足,那么socket的状态就会发生变化,从而循环再也不阻塞,并且句柄也能够读、写及执行其余指望的I/O操做。

更好的理解事件循环操做如何进行,下图展现了一个循环迭代的全部阶段。

文件 I/O 与网络 I/O 不一样 ,并不存在 libuv 能够依靠的各特定平台下的文件 I/O 基础函数,因此目前的实现是在线程中执行阻塞的文件 I/O 操做来模拟异步。

注意:libuv利用线程池技术使得异步文件I/O操做称为可能,可是对于网络IO只能执行在一个单一线程中,即loop的线程中。


3、Event Loop

任务队列

异步任务分为task(宏任务,也可称为macroTask)和microtask(微任务)两类。 当知足执行条件时,task和microtask会被放入各自的队列中等待放入主线程执行,咱们把这两个队列称为Task Queue(Macrotask Queue)和Microtask Queue。

MacroTask(宏任务)

script代码setTimeoutsetIntervalsetImmediate(浏览器IE10)MessageChannelI/OUI-Rendering

MicroTask(微任务)

Process.nextTick(Node独有)PromiseMutationObserverObject.observe(废弃)

一、 浏览器 E-L

JS调用栈采用的是先进后出原则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

  • 执行栈在执行完同步任务后 ,查看执行栈是否为空,若是执行栈为空,就会去检查微任务microTask队列是否为空,若是为空的话,就执行Task(宏任务),不然执行微任务。
  • 每当单个宏任务执行完毕后 ,检查microTask队列是否为空,若是不为空,会按照 先入先出 原则所有执行microTask队列,设置microTask队列为null,而后再执行宏任务,如此反复。
console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

// script start、script end、promise一、promise二、setTimeout
复制代码

Another One

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
}).then(function() {
    console.log('promise1')
}).then(function() {
    console.log('promise2')
})

console.log('script end')
复制代码
  • async/await 在底层转换成了 promisethen 回调函数。
  • 每次咱们使用 await, 解释器都建立一个 promise 对象,而后把剩下的 async 函数中代码的操做放到 then 回调函数中。

关于Chrome73如下版本和73版本的区别

  • 在老版本版本如下,先执行promise1promise2,再执行async1。 script start、async2 end、Promise、script end、promise一、promise二、async1 end、setTimeout

  • 在73版中,先执行async1再执行promise1promise2。 script start、async2 end、Promise、script end、async1 end、promise一、promise2、setTimeout

主要缘由是由于在谷歌73版本中更改了规范

二、 Node E-L

在Node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
复制代码
timers:执行知足条件的setTimeout、setInterval回调。
I/O callbacks:是否有已完成的I/O操做的回调函数,来自上一轮的poll残留。
idle,prepare:可忽略
poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
check:执行setImmediate的回调。
close callbacks:关闭全部的closing handles,一些onclose事件。
复制代码

咱们须要重点关心的是timerspollcheck这三个阶段。

1. timers 执行setTimeoutsetInterval中到期的callback,执行这二者回调须要设置一个毫秒数,理论上来讲,应该是时间一到就当即执行callback回调,可是因为system的调度可能会延时,达不到预期时间。

2. poll 执行I/O回调 和 处理轮询队列中的事件。

① 若是 poll 队列不是空的,event loop 就会依次执行队列里的回调函数,直到队列被清空或者到达 poll 阶段的时间上限。

② 若是 poll 队列是空的,就会:

  1. 有 setImmediate 任务,event loop 就结束 poll 阶段去往 check 阶段。
  2. 没有 setImmediate 任务,event loop 就会等待新的回调函数进入 poll 队列,并当即执行它。

3. check 此阶段容许人员在poll阶段完成后当即执行回调。

setImmediate()其实是一个特殊的计时器,它在事件循环的一个单独阶段运行。它是经过 libuv 里一个能将回调安排在 poll 阶段以后执行的 API 实现的。

在poll队列是空的 且有 setImmediate 任务的状况下,event loop 就结束 poll 阶段去往 check 阶段执行任务。

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
复制代码

若是node版本为v11.x, 其结果与浏览器一致:

start
end
promise3
timer1
promise1
timer2
promise2
复制代码

v10若是time2定时器已经在执行队列中结果为:

start
end
promise3
timer1
timer2
promise1
promise2
复制代码

不然和第一个结果一致。

了解浏览器的eventloop可能就知道,浏览器的宏任务队列执行了一个,就会执行微任务。

简单的说,能够把浏览器的宏任务和node10timers比较,就是node10只有所有执行了timers阶段队列的所有任务才执行微任务队列,而浏览器只要执行了一个宏任务就会执行微任务队列。

node11保持和浏览器相同。


1. setImmediate && setTimeout

setImmediate和setTimeout是类似的,但根据它们被调用的时间以不一样的方式表现。

setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本 。 setTimeout()为最小(ms)后运行的脚本,在timers阶段执行。

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

// timeout,immediate
// immediate,timeout
复制代码
const fs = require('fs');

fs.readFile('../file.txt', () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
// immediate,timeout
复制代码

2. Process.nextTick

process.nextTick()虽然它是异步API的一部分,但从技术上讲,它不是事件循环的一部分。

process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务所有完成,在next tick队列中的全部callbacks会被依次调用。

当每一个阶段完成后,若是存在 nextTick 队列,就会清空队列中的全部回调函数,而且优先于其余 microtask 执行。

Promise.resolve().then(() => console.log('Promise'));
  process.nextTick(() => console.log('nextTick'));
  // nextTick
  // Promise
复制代码
setImmediate(() => {
  console.log('setImmediate1');
  setTimeout(() => {
    console.log('setTimeout1');
  }, 0);
});

setTimeout(() => {
  process.nextTick(() => console.log('nextTick'));
  console.log('setTimeout2');
  setImmediate(() => {
    console.log('setImmediate2');
  });
}, 0);

//结果1
// setImmediate1
// setTimeout2
// setTimeout1
// nextTick
// setImmediate2

// 结果2
// setTimeout2
// nextTick
// setImmediate1
// setImmediate2
// setTimeout1
复制代码

JavaScript是单线程的,但Node自己实际上是多线程的,除了用户代码没法并行执行外,全部的I/O请求是能够并行执行的。 事件循环是Node异步I/O实现的核心,Node经过事件驱动的方式处理请求,使得其无须为每一个请求建立额外的线程,省掉了建立和销毁线程的开销。同时也由于线程数较少,不受线程上下文切换的影响,维持了Node的高性能。 Node异步IO、非阻塞的特性,使它很是适用于IO密集、高并发的应用场景。


参考文章:

相关文章
相关标签/搜索