Event Loop
Event Loop 介绍
event loop是一个执行模型,在不同的内核中有不同的实现。浏览器和nodejs基于不同的技术实现了各自的event loop
Event Loop 原理
1、大多数现代内核都是多线程的,由于javascript是为浏览器而生,主要目的就是为用户提供前后台交互机制和操作DOM。为了提高执行效率避免复杂性,js被设计成单进程语言。这种设计是js的一大特点将来也不会改变。
2、但是单线程的程序会有一些很棘手的问题,比如程序在执行一个大的io或者正在执行一个性能较低的网络操作时程序会被阻塞直到任务执行完成。CPU的利用率会大大降低,因为在任务等待期间CPU也是闲着的。这样的设计会大大降低宝贵的cpu资源使用率。
3、为了解决这个问题,js引入了Event Loop的概念。其实如果有其他编程语言基础很容易就能想到解决方案,那就是添加一个线程去处理不使用cpu资源而是使用其他资源的任务,比如说网络设备或io设备。这个工作js无法实现原因还是其单线程机制,所以这个工作就要依靠内核(js解释器)去实现,所有同步任务都在主线程上执行,形成一个执行栈
(Execution Context Stack)。当一个任务(同步的或异步的)正在执行的时候,后续的任务都要等待,这也就是阻塞式执行
。等待的异步任务按照程序插入的先后顺序排列成一个任务队列
,一旦执行栈中的所有同步任务执行完毕,引擎就会读取任务队列,然后将任务队列中的第一个任务压入执行栈中运行。所有io任务、网络任务、用户单机任务、定时器任务等异步任务都会插入到队列中。
此时发生了一次鼠标点击,那么事件处理函数都被阻塞,用户也无法看到反馈,直到前面的代码都执行完后才会开始执行事件。
4、只要主线程空闲,任务队列会被循环读取这也就是Event Loop程序要做的事情
5、图为执行栈和任务队列简易图
- 主线程运行时会产生执行栈,
- 栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
- 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
- 如此循环
注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
浏览器
浏览器的event loop则在html5的规范中明确定义,然后由各大浏览器厂家自己实现。
nodeJs
nodejs的event loop是基于libuv
在nodejs中任务队列
中的异步处理任务并不是随机插入到队列中,在nodejs中根据模型的先后顺序,按照事件类型有序地插入到队列中。event loop根据先后顺序循环扫描队列模型,假如io完成后的回调需要执行,或者计数器时间片到达时的回调需要执行。内核将程序回调重新压入到栈中执行,执行完后抛出直到栈空,event loop再去扫描任务队列,循环往复。
图中表示的是队列模型:
- timers:
计时器
此阶段执行由setTimeout()和setinterval安排的回调
计时器回调可以指定需要执行的阈值,在阈值之后执行指定的回调,而不是人员希望执行的确切时间。由于event loop自身的原因(
阻塞式执行
循环存在,操作系统调度或其他回调的运行可能会延迟它们。)计时器回调不一定就能在确定的阈值后执行回调,确切地说应该是在大于等于设定的阈值时间后执行回调。
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100); // 我们希望在100m后执行回调并打印
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
1、解释器从js上下文的头部扫描执行
2、const fs = require(‘fs’); 入栈
3、函数ncOperation(callback)进入堆中
4、const timeoutScheduled = Date.now();入栈
5、setTimeout()的回调进入认为队列的计时器
模块等待执行
6、someAsyncOperation入栈
7、此时js已经到达最底部栈中的程序开始依次弹出执行,如果程序体中依然有函数嵌套那么函数以通用的方法压入栈中,并在上一个任务执行完成后弹出(弹出的过程是把任务丢给处理器,处理器执行完成后才可以抛弃。)
8、当栈中弹出的任务是someAsyncOperation时在函数体中发现有fs.readFile('/path/to/file', callback);
,根据规则I/O操作的回调需要进入任务队列的相应模块。此时栈中任务结束。事件循环程序 开始扫描任务队列。
9、此时的任务队列中在timer中有一个setTimeout
在poll中有一个fs.readFile
。第一次循环按照模型顺序检查timer中的任务发现没有到达阈值,接向下执行直到发现poll中的I/O任务,执行读文件操作假如用去95ms接着执行回调经过入栈出栈执行的过程后假如用去10ms,此时I/O任务执行完毕。进入第二次循环取出timer中的定时器发现时间片已经到达阈值设定的时间,执行定时回调。
10、经过第9步的分析发现虽然我们设置了定时器的等待时间是100ms但是由于I/O操作及其回调用去了95ms+10ms=105ms的时间,最快定时器也要在105ms执行了。
- pending callbacks:
挂起回调
执行推迟到下一个循环迭代的I/O回调
这个阶段执行一些系统操作的回调,比如TCP错误类型。如果TCP套接字在尝试连接时收到ECONNREFUSED,则某些* nix系统希望等待报告错误。这将在挂起的回调阶段排队执行。
- idle, prepare: 仅用于内部
- poll: 检索新的I/O事件;执行与I/O相关的回调(几乎是所有回调;关闭回调、定时器调度的回调、setImmediate()除外);节点会在适当的事件阻塞在这里
该阶段有两个主要功能
1、计算在阻塞多长时间后轮询I/O
2、处理轮询队列中的事件
当event loop进入poll时将发生两种情况
1、如果poll不为空,则event loop将在同步执行他们的回调队列中循环迭代,直到队列耗尽或到达操作系统的硬限制。
2、如果poll为空,则又会发生两种情况
(1)、如果存在setImmediate脚本、则event loop将进入check
阶段执行这些脚本
(2)、如果不存在setImmediate脚本,则event loop将等待回调添加到队列中,然后立即执行它们。
注意:一旦poll queue为空,event loop将检查达到时间阈值的计时器。如果一个或多个计时器准备就绪,事件循环将返回到计时器阶段,以执行这些计时器的回调。
- check: setImmediate() 回调
setImmediate()实际上是一个特殊的计时器,在Event loop的一个单独阶段运行。它使用一个libuv API,该API计划在轮询阶段完成后执行回调。
通常,在程序中,Event Loop会进入poll的轮询阶段,在此阶段它将等待传入的连接、请求等。但是如果存在setImmediate,且poll阶段变为空闲,则轮询将结束并进入check。
- close callbacks: 一些关闭回调, 例如 socket.on(‘close’, …).
如果socket或 操作句柄被突然关闭(socket.destroy()),将在此阶段发出‘close’事件,否则它将被
process.nextTick()
触发。
注意: 在事件循环之前Node.js会检查有没有定时器或任何异步I/O操作,如果当前程序已经没有了任何需要异步的任务,那么循环就会关闭。