我们按照任务设定的执行时间,将这些任务存储在优先级队列中,队列首部(也就是小顶堆的堆顶)存储的是最先执行的任务。
这样,定时器就不需要每隔 1 秒就扫描一遍任务列表了。它拿队首任务的执行时间点,与当前时间点相减,得到一个时间间隔 T。
这个时间间隔 T 就是,从当前时间开始,需要等待多久,才会有第一个任务需要被执行。这样,定时器就可以设定在 T 秒之后,再来执行任务。从当前时间点到(T-1)秒这段时间里,定时器都不需要做任何事情。
3.1 Nodejs 14 的 setTimeout 实现
起点位于lib/timers中setTimeoout函数:
function setTimeout(callback, after) {
const timeout = new Timeout(callback, after);
insert(timeout, timeout._idleTimeout);
return timeout;
}
{
const { getTimerCallbacks } = require('internal/timers');
const { setupTimers } = internalBinding('timers');
const { processImmediate, processTimers } = getTimerCallbacks(runNextTicks);
// Sets two per-Environment callbacks that will be run from libuv:
// - processImmediate will be run in the callback of the per-Environment
// check handle.
// - processTimers will be run in the callback of the per-Environment timer.
setupTimers(processImmediate, processTimers);
}
快结束了,接下来看internal/timer.js中的processTimers就好了
function processTimers(now) {
nextExpiry = Infinity;
let list;
let ranAtLeastOneList = false;
// 从小顶堆中取出根节点,即最快到期的节点
while (list = timerListQueue.peek()) {
// 还没过期
if (list.expiry > now) {
nextExpiry = list.expiry;
return refCount > 0 ? nextExpiry : -nextExpiry;
}
// list是具有相同过期时间的链表
listOnTimeout(list, now);
}
return 0;
}
function listOnTimeout(list, now) {
const msecs = list.msecs;
let ranAtLeastOneTimer = false;
let timer;
// 遍历这个链表
while (timer = L.peek(list)) {
// 计算已经过去的时间
const diff = now - timer._idleStart;
// 过去的时间比延迟时间小,说明没过期
if (diff < msecs) {
// 调整到期时间
list.expiry = MathMax(timer._idleStart + msecs, now + 1);
list.id = timerListId++;
// 调整到期时间后,下沉处理
timerListQueue.percolateDown(1);
return;
}
// 移除这个节点
L.remove(timer);
// 执行timer的_onTimeout回调
try {
const args = timer._timerArgs;
if (args === undefined)
timer._onTimeout();
else
timer._onTimeout(...args);
} finally {
// 省略
}
}
// 为空则删除
if (list === timerListMap[msecs]) {
delete timerListMap[msecs];
timerListQueue.shift();
}
}
1. 关于堆的算法知识
堆分为:
堆常见操作:
heapify
: 把一个乱序的数组变成堆结构的数组,时间复杂度为O(n)push
: 把一个数值添加进堆结构,并保持堆结构,时间复杂度为O(log n)pop
: 去除根节点,并保持堆结构,时间复杂度O(log n)sort
: 堆排序,时间复杂度O(nlog n),空间复杂度O(1)父节点与左右节点的索引关系如图:
为什么快速排序要比堆排序性能好:
关于堆排序:
1.1 交换操作(swap)
交换操作:通常用于交换父子节点或者首尾节点
1.2 比较操作(less)
比较操作:确定大顶堆还是小顶堆,虽然常见的命名是less,但并不是小于符号的意思。
1.3 push会用到的上浮(siftUp)操作
push
往堆里添加任意元素而又不能破坏堆的性质,步骤如下:less(当前节点值, 父节点值)
,则交换上方代码使用递归,感觉比 while 循环更好理解,逻辑就是这么个逻辑。
1.4 pop会用到的下沉(siftDown)操作
pop
删除根节点而又不能破坏堆的性质,步骤如下:1.5 堆化操作(heapify)与堆排序(heapSort)操作
对一个初始数组进行堆化操作:以最后一个叶子节点的父节点为起点,把起点到根节点之间的所有节点进行下沉操作
假设此处得到一个大顶堆,排序操作如下:
1.6 实现
组合一下代码即可:
2. 堆的实际应用一: 利用堆求中位数
抛出一个压力测试的实例:下图中的P99、P50等数据是如何得出的?显然是堆
2.1 leetcode295 - 数据流中的中位数
步骤:
lo
hi
k = 2n
: 每个堆都包含 n 个元素,中间值可以从两个堆的堆顶计算出k = 2n+1
: 最大堆 n+1 个,中间值为最大堆的堆顶num
, 此时最大堆、最小堆数量可能失衡,将会有两种操作:lo
中移除最大的元素并将其提供给hi
hi
中移除最小的元素并将其提供给lo
Golang题解:
3. 堆的实际应用二: 高性能定时器
更好的解决办法当然是堆:
3.1 Nodejs 14 的 setTimeout 实现
起点位于
lib/timers
中setTimeoout
函数:lib/internal/timers
中Timeout
构造函数和insert
函数然后就是 c++ 的
scheduleTimer
函数,看不懂也关系,我也看不懂。总之作用就是:c++层面开启一个计时器,会在事件循环的 timer 阶段判断是否过期,是的话执行
RunTimers
函数nodejs在初始化时对定时器进行了初始化工作,
lib/internal/bootstrap/node.js
快结束了,接下来看
internal/timer.js
中的processTimers
就好了4. 堆的实际应用三: 求Top K
实际例子比如:"如何快速获取到Top 10最热门搜索关键字", 具体看leetcode一道题就成了
4.1 leetcode373 - 查找和最小的k对数字
这事见得多了,我只想说懂得都懂,不懂的我也不多解释,自己知道就好,细细品吧。 你们也别来问我怎么了,利益牵扯太大,说了对你我都没好处,当不知道就行了,其余的我只能说这里面水很深, 牵扯到很多东西。详细情况你们自己是很难找的,网上大部分已经删除干净了,所以我只能说懂得都懂。
参考
极客时间 - 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
nodejs 14.0.0源码分析之setTimeout