在阅读 libevent 库中另外一个用来存储事件的第二个结构就是小根堆,这里重新学习一下它的实现
代码路径:minheap-internal.h
1 | typedef struct min_heap |
struct event** p
区别与使用数组进行存储,这样就可以实现动态扩容
堆初始化
一开始没有元素,而要做的就是初始化一个最小堆对象 min_heap_t
1 | //event.c:650 event_base_new_with_config |
初始化堆,这里有一个小技巧就是将 s->p= 0
, 就是不指向任何对象,相当于 NULL,min_heap_t* a = 0;
,同时堆的分配内容大小 a 一开始并没指定**(懒加载形式)**
其他辅助函数包括
1 | int min_heap_empty_(min_heap_t* s) { return 0 == s->n; } //堆是否为空 |
添加节点
1 | //event.c:2217 event_assign |
在添加节点的时候,首先指定节点在堆中的索引是 EV_SIZE_MAX
,表示并没有加入到堆中,接着加入到堆中
1 | int min_heap_push_(min_heap_t* s, struct event* e) |
这里就发生了两步操作
- 因为可能堆已经满了,所以需要将堆进行扩容
min_heap_reserve_
- 堆化,需要调整成小顶堆
扩容
首次分配的容量大小是8,后续依次加倍
问题1:如果节点新增很多,会不会导致内存分配暴涨?
golang 的 map 有一个 2倍增长到了一定数量再1.25倍增长的过程
1 | int min_heap_reserve_(min_heap_t* s, size_t n) |
- 当容量不够的时候,需要进行扩容操作,不需要考虑 golang map 那种因为装载量过大而影响查询效率的情况
- 容量首先是8,然后依次递增,当等待事件比较多,注意内存占用(一大块内存)
- 使用
realloc
操作,直接保留了原来的内容(realloc 的底层并不一定在原有的基础上,因为无法保证内存后面是否仍然有足够的空闲空间)
向上调整
元素入堆的基本流程是:
-
将元素添加到堆尾(堆最后一个元素)
-
将元素与父节点进行比较
a. 如果比父节点大则与父节点交换位置 然后重复 1~2 操作
b. 如果不比父节点大则停止入堆,完成元素添加
接着看它的实际操作
1 | //min_heap_shift_up_(s, s->n++, e); |
hole_index
一开始为堆的最后一个元素的索引,即当前事件 e 的位置- 找到最后一个节点的父节点
(hole_index - 1) / 2;
- 如果当前位置
hole_index
还不是堆顶(hole_index != 0
)且 父节点超时时间比自己大- 当前元素等于父节点,索引位置更新为
hole_index
- 当前索引
hole_index
为父节点索引,hole_index = parent;
- 找到父节点位置
- 重复操作 3
- 当前元素等于父节点,索引位置更新为
- 更新当前节点为e,设置索引为
hole_index
比较逻辑则是使用超时时间进行对比
1 |
|
弹出节点
有增加就有删除,出堆常规操作是
- 将堆顶与堆尾元素进行交换
- 将堆尾元素出堆
- 从对顶元素开始从上到下,与子节点比较交换
看看它是如何做到的
1 | struct event* min_heap_pop_(min_heap_t* s) |
--s->n
因为要弹出,所以 数量减 1,s->p[--s->n]
指向堆尾- 如果堆元素数量大于 0,记录堆顶事件。否则返回 0(与前面一样,0 指向指针表示 NULL)
- 向下调整,注意最后一个节点指向的是最后一个节点
- 更新出堆事件的索引(表示不在堆里面)
向下调整
将 hole_index
与 struct event* e 进行交换
1 | void min_heap_shift_down_(min_heap_t* s, size_t hole_index, struct event* e) |
-
找到节点的右子节点
min_child
-
如果右节点存在(不应该超过数目上线)
-
找到左右节点种较大的
-
如果当前超时时间比子节点小则停止
为什么有
min_child == s->n
的比较操作,感觉没有必要!!因为while (min_child <= s->n)
已经判断了 -
当前节点为子节点,并更新索引
-
更新当前节点位置
-
找到当前节点的右子节点
-
-
当前位置设置为 e,并更新事件的索引
删除元素
删除元素常用于事件发生异常,需要从堆中直接删除而不是从堆顶,这个时候就会出现堆空洞的情况
1 | int min_heap_erase_(min_heap_t* s, struct event* e) |
- 如果
e->ev_timeout_pos.min_heap_idx != EV_SIZE_MAX
表示已经不在堆中了,添加和弹出的时候都会更新这个索引 - 获取堆尾元素
- 获取事件的父节点
- 如果事件不在堆顶而且父结点大于最后一个节点
- 向上调整
- 如果事件在堆顶,类似
min_heap_pop_
- 向下调整
向上调整2
这里为了防止空洞,出现了一个不一样的操作 min_heap_shift_up_unconditional_
1 | void min_heap_shift_up_unconditional_(min_heap_t* s, size_t hole_index, struct event* e) |
- 首先找到当前元素的父节点
- 将父节点更新到当前位置,并跟新索引
- 设置当前位置为父节点,找到父节点的父节点
parent2
- 如果
parent2
不是堆顶,而且parent2
大于 last,那么重复 2~3操作(感觉不会,因为是小根堆) - 更新 last 到合适的位置
也就是说通过 将 last 填补到要删除元素的位置,解决空洞问题。
问题1:min_heap_shift_up_unconditional_
与 min_heap_shift_up_
不同的是,后者先判断了 当前位置以及 当前位置事件与 e 的大小,为什么要做这个区别?
答:是因为前者的位置肯定不是堆顶(走乡下条)
问题2:min_heap_erase_
删除元素中为什么会出现向下调整与向上调整
答:首先需要明白向上调整与向下调整的作用分别是什么,
向下调整:被用于在删除堆顶元素后,将最后一个元素移到堆顶并逐步将其“下沉”至合适的位置,以保持堆的有序性质。
向上调整:被用于在向堆中插入新元素后,将其逐步“上浮”至合适的位置,以保持堆的有序性质
如果删除位置不是堆顶元素,且父节点大于 last 。那么根据小根堆的性质,last 应该从这个位置开始向上调整
如果删除位置是堆顶元素或者父节点小于last。那么根据小根堆的性质,last应该在删除位置的下发(或不变)