min_heap类型定义

min_heap函数

构造/析构函数及初始化

判断event是否在堆顶

判断两个event之间超时结构体的大小关系

判断堆是否为空及堆大小

返回堆顶event

分配堆空间

堆元素的上浮

堆元素的下沉

堆插入元素

堆删除元素

弹出堆顶元素

以下源码均基于libevent-2.0.21-stable。

       在libevent中,使用min_heap这一数据结构来管理各个event的超时,也就是小顶堆,整个堆是根据各个event的超时时间来构成的,因此堆顶肯定就对应超时时间最小的event,这样就可以按照超时顺序进行处理了。

       关于小顶堆的性质,可以先参考堆排序中有关大顶堆的描述。
min_heap类型定义

       libevent中的min_heap是一个结构体类型,定义如下:

    typedef struct min_heap
    {
        struct event** p; //小顶堆的首地址
        unsigned n, a; //n为event *元素个数,a是event指针链表的长度(以event *p为单位)
    } min_heap_t;

       这里涉及到了3个成员变量,对于struct event ** p,这是一个二级指针,它指向一个struct  event *型的变量,而struct event *的话,就应该很熟悉了,它指向一个事件event。通过p这个二级指针也就可以实现小顶堆对应的数组了,原因在于:在C语言中,p[i]是等价于*(p+i)的,而p作为struct event的二级指针,其指向的元素类型为struct event *型,因此 p+i 实际上就是从首地址开始偏移到第i个struct event *型元素,即p+i在数值上就等于(int)p+i*sizeof(struct event *),即p+i为第i个struct event *型元素的地址,因此*(p+i)就是第i个struct event *型元素了,因此,对于struct event ** p来说,p[i]就表示第i个struct event *元素,偏移量为i*sizeof(struct event *)。这样看来struct event ** p实际上就相当于struct event *p[capacity]指针数组,之所以不使用指针数组而是使用二级指针,是因为C语言中不存在像C++中vector那样长度可变的动态容器,如果定义为数组必须指定数组大小,这是不符合要求的,因此直接使用二级指针,在需要添加元素的时候用malloc来分配一个所需大小的内存空间即可。

       而对于另外两个成员变量,我们暂且把p作为一个数组,其中每个元素都是strcut event *类型,那么n就表示这个p数组中当前的元素个数,而a则表示p数组最多能容纳的元素个数。(不得不吐槽一下这里变量的命名。。。)这就像C++中vector的size和capacity的区别,前者表示当前容器中的元素个数,后者表示当前容器最多能容纳的元素个数。

       在正式分析函数前,还需要知道的是,每个event中,都定义了一个min_heap_idx用来存储event在这个小顶堆p数组中的索引,虽然堆中的每个元素都是一个event指针,但是建堆的依据是这些event各自设置的超时结构体ev_timeout,这是定时器小顶堆实现依据。如下所示。

    struct event {
        ......
        union {
            ......
            int min_heap_idx;  //event在堆中的索引
        } ev_timeout_pos;
        ......
        struct timeval ev_timeout;  //超时时间
        ......
    };

min_heap函数
构造/析构函数及初始化

       虽然说C语言中没有构造函数和析构函数,但是min_heap也将这种思想进行了体现在了min_heap_ctor函数和min_heap_dtor函数上,从函数名上看就是constructor和destructor的简写,各自定义如下:

    void min_heap_ctor(min_heap_t* s) { s->p = 0; s->n = 0; s->a = 0; }//构造函数 初始化
    void min_heap_dtor(min_heap_t* s) { if (s->p) mm_free(s->p); } //析构函数 释放空间

       min_heap_elem_init函数用来初始化小顶堆中的event,将event的堆索引初始化为-1。其定义如下:

void min_heap_elem_init(struct event* e) { e->ev_timeout_pos.min_heap_idx = -1; }

判断event是否在堆顶

      min_heap_elt_is_top函数用于完成这一功能。显然,如果event的堆索引为0,那么这个event就在堆顶了。其定义如下:

    int min_heap_elt_is_top(const struct event *e)//判断event的超时是否在定时器堆顶
    {
        return e->ev_timeout_pos.min_heap_idx == 0;
    }

判断两个event之间超时结构体的大小关系

       min_heap_elem_greater函数传入两个event参数,用来判断第一个参数event的超时结构体是否大于第二参数的超时结构体,如果大于则返回1,否则返回0。比较两个超时结构体先比较秒数,再比较微妙数,函数中调用了宏函数,如下所示:

    #define    evutil_timercmp(tvp, uvp, cmp)                    \
        (((tvp)->tv_sec == (uvp)->tv_sec) ?                \
         ((tvp)->tv_usec cmp (uvp)->tv_usec) :                \
         ((tvp)->tv_sec cmp (uvp)->tv_sec))
     
     
    int min_heap_elem_greater(struct event *a, struct event *b)
    {
        return evutil_timercmp(&a->ev_timeout, &b->ev_timeout, >);//先比较sec是否a大于b,如果a和b的sec相同,就比较usec;
    }

判断堆是否为空及堆大小

       前面说了min_heap中的成员变量n描述堆中实际存在的元素数目,因此直接判断n是否为0即可:

    int min_heap_empty(min_heap_t* s) { return 0u == s->n; }   //堆是否为空
    unsigned min_heap_size(min_heap_t* s) { return s->n; }   //堆大小

返回堆顶event

struct event* min_heap_top(min_heap_t* s) { return s->n ? *s->p : 0; }

分配堆空间

      min_heap是在插入新的event时,如果空间不足是可以自动扩容的,该函数需要传入n表明需要让堆装下n个元素。由函数min_heap_reserve实现如下:

    int min_heap_reserve(min_heap_t* s, unsigned n)
    {
        if (s->a < n)
        {
            struct event** p;
            unsigned a = s->a ? s->a * 2 : 8;//如果堆中本身是空的,就直接分配为8,否则就直接加倍,这样就防止每add一个event都需需要realloc
            if (a < n)//如果加倍仍然无法满足条件,就直接用n
                a = n;
            if (!(p = (struct event**)mm_realloc(s->p, a * sizeof *p))) //realloc分配内存
                return -1;
            s->p = p; //保存分配内存的地址
            s->a = a; //保存堆的容量
        }
        return 0;
    }

       前面说过,min_heap的成员变量a描述的是堆最大所能容纳的元素数目,也就是堆的容量。如果传入的n本身小于a,说明当前堆完全可以装下n个元素,因此无需再扩容了。

       如果传入的n不小于a,说明此时的堆刚刚能装下或者装不下n个元素,此时就需要对堆进行扩容。min_heap这里分了两种情况:如果堆本身为空,那么就直接为堆分配8个元素的空间;如果堆本身不为空,那么就先将堆原本的空间加倍,作为堆的新容量,如果堆非空时加倍之后或者堆空时分配8个元素空间还放不下n个元素,那么就直接把n作为堆的新容量。这样做的好处是不用每次插入一个新的event都去重新分配空间。

       此外,如果min_heap需要分配更大的空间,这里使用的是realloc函数,会先调用malloc函数进行指定大小空间的分配,再把原来的内存数据复制到新空间中。
堆元素的上浮

       在小顶堆(大顶堆)中,当堆中元素需要进行调整时,就会对相应的元素进行上浮或者下沉,之所以要这样做,是因为堆中元素调整后不一定还满足小顶堆(大顶堆)的性质,因此就要重新进行调整,让堆重新满足原来的特性。

       min_heap的堆元素上浮是通过min_heap_shift_up_函数实现的,该函数定义如下:

    //hole_index是需要调整的结点索引
    void min_heap_shift_up_(min_heap_t* s, unsigned hole_index, struct event* e)
    {
        unsigned parent = (hole_index - 1) / 2;  //找到其父节点的索引
        //如果父节点的超时值大于当前event结点的超时值,不满足小顶堆性质,就上浮
        while (hole_index && min_heap_elem_greater(s->p[parent], e))  
        {
        //将原来的父节点event换到hole_index的位置上并改变父节点event的堆索引值
        (s->p[hole_index] = s->p[parent])->ev_timeout_pos.min_heap_idx = hole_index;
        hole_index = parent;  //此时就上浮到了parent的位置,现在以parent出发继续判断
        parent = (hole_index - 1) / 2;  //计算新的父节点索引
        }
        //执行到这里hole_index就是需要调整的event的最终位置,然后就直接将event放到该位置并设置event中的堆索引值即可
        (s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
    }

       上浮,顾名思义就是判断当前结点与其父节点的关系是否满足小顶堆的性质,如果不满足那么就应当将当前结点和父节点互换,然后从当前结点的新位置出发继续上浮,直到结点关系满足小顶堆性质为止。
堆元素的下沉

       与堆元素的上浮相似,由min_heap_shift_down_函数实现,其定义如下:

    //hole_index为需要调整的event的堆索引
    void min_heap_shift_down_(min_heap_t* s, unsigned hole_index, struct event* e)
    {
        unsigned min_child = 2 * (hole_index + 1);  //计算右子结点的堆索引
        while (min_child <= s->n) //如果右子结点存在
        {
        //如果右子结点超时值大于左子结点或者只有左子结点,那么左子结点值就是较小的(或唯一的),此时就只用比较左子结点和当前结点,否则就比较当前结点和右子结点
        min_child -= min_child == s->n || min_heap_elem_greater(s->p[min_child], s->p[min_child - 1]);
        //到这里min_child的值就是左右子结点中较小结点的索引
        if (!(min_heap_elem_greater(e, s->p[min_child]))) //如果当前结点就是三个结点中的最小值说明满足小顶堆性质,无需下沉直接退出,否则就往下调整
            break;
        //将较小结点赋值到当前结点,并修改其堆索引
        (s->p[hole_index] = s->p[min_child])->ev_timeout_pos.min_heap_idx = hole_index;
        hole_index = min_child;//更新hole_index到原最小结点的索引
        min_child = 2 * (hole_index + 1);  //继续计算右子结点索引
        }
        //此时已经找到合适的位置,直接更新event的索引及位置。
        (s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
    }

       堆元素的下沉与上浮是差不多的,只不过上浮是从子结点出发判断子结点与父节点的关系进行调整,而下浮则是从父节点出发判断父节点和子结点的关系进行调整。如果下沉过程中当前结点不是它自身与其左右子结点三者间的最小值,那么就将当前结点与最小结点进行互换,然后互换后的当前结点继续从新位置出发下沉调整,直到满足小顶堆性质。
堆插入元素

       向堆中插入一个新元素,由min_heap_push实现,其定义如下:

    int min_heap_push(min_heap_t* s, struct event* e)//向堆中添加event指针
    {
        if (min_heap_reserve(s, s->n + 1))  //为待插入的event重新分配一个位置
            return -1;
        min_heap_shift_up_(s, s->n++, e);  //虽然heap空间可能加倍,但是还是从当前heap的有效结点的后一个位置插入event,然后上浮,push后n加1
        return 0;
    }

      由于是向堆中插入元素,因此需要先使用min_heap_reserve函数来为新插入元素分配足够大小的堆内存。新元素的位置实际上是之前最有一个“有效元素”的后面一个,这里的“有效元素”只是为了说明新插入元素的位置并非是min_heap分配空间中的最后一个位置。由于是从堆的“尾部”插入一个新元素,那么自然就需要调整该元素,进行“上浮 ”操作。
堆删除元素

       需要注意的一点是,由于堆末尾的元素对于整个堆来说,删除它对于堆是没有任何影响的,因此,如果要对堆中的任意一个元素进行删除,就可以将需要删除的元素先和堆尾元素互换,然后不考虑需要删除的元素,对互换后的堆进行调整,最终得到的堆就是删除了该元素的堆了。由min_heap_erase实现,由于其定义如下:

    int min_heap_erase(min_heap_t* s, struct event* e)//
    {
        if (-1 != e->ev_timeout_pos.min_heap_idx)//堆索引为-1表示不在堆上
        {
            struct event *last = s->p[--s->n]; //获取堆中的最后一个元素
            unsigned parent = (e->ev_timeout_pos.min_heap_idx - 1) / 2; //找到需要删除的结点的父节点的堆索引
            /* we replace e with the last element in the heap.  We might need to
               shift it upward if it is less than its parent, or downward if it is
               greater than one or both its children. Since the children are known
               to be less than the parent, it can't need to shift both up and
               down. */
            //如果要删除的event不在堆顶,并且最后一个结点的超时值小于父节点的超时值
            if (e->ev_timeout_pos.min_heap_idx > 0 && min_heap_elem_greater(s->p[parent], last))
                min_heap_shift_up_(s, e->ev_timeout_pos.min_heap_idx, last); //相当于把最后一个event换到了要删除的结点位置,此时换过来的结点及其子结点必然也是满足小顶堆性质的,因此从该结点出发进行上浮调整
            else //如果要删除的event本身就是堆顶,或者最后一个结点的超时值不小于父节点的超时值,就将最后一个结点的超时值换到要删除的结点位置,然后下沉
                min_heap_shift_down_(s, e->ev_timeout_pos.min_heap_idx, last);
            e->ev_timeout_pos.min_heap_idx = -1;  //被删除的结点堆索引值重置为-1
            return 0;
        }
        return -1;   //说明需要删除的结点本身就不在堆上
    }

弹出堆顶元素

       弹出堆顶元素和返回堆顶元素时两码事,前者会改变堆,而后者则只是查询。弹出堆顶元素实际上就是删除堆顶元素,由min_heap_pop函数实现,其定义如下:

    struct event* min_heap_pop(min_heap_t* s)
    {
        if (s->n)
        {
            struct event* e = *s->p;  //找到第一个元素
            min_heap_shift_down_(s, 0u, s->p[--s->n]);  // --s->n为最后一个结点的的堆索引,这就相当于将最后一个event换到堆索引为0的位置,然后下沉调整这个堆,调整后堆顶就是新的最小值了
            e->ev_timeout_pos.min_heap_idx = -1;  //弹出后堆索引重置为-1
            return e;
        }
        return 0;  //如果堆空就返回NULL
    }

 
————————————————
版权声明:本文为CSDN博主「HerofH_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_28114615/article/details/95342338

libevent源码学习(10):min_heap数据结构解析的更多相关文章

  1. libevent源码学习

    怎么快速学习开源库比如libevent? libevent分析 - sparkliang的专栏 - 博客频道 - CSDN.NET Libevent源码分析 - luotuo44的专栏 - 博客频道 ...

  2. PHP 源码学习 | 变量类型数据结构

    前段时间因为项目需要,研究了一下在 Windows 系统下进行 PHP 扩展的开发,对于 PHP 扩展的开发并不是一件容易的事情(话又说回来了,会者不难,难者不会,关键是自己不会).我当时的需求,主要 ...

  3. # 曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. libevent源码学习(11):超时管理之min_heap

    目录min_heap的定义向min_heap中添加eventmin_heap中event的激活以下源码均基于libevent-2.0.21-stable.       在前文中,分析了小顶堆min_h ...

  5. libevent源码学习(8):event_signal_map解析

    目录event_signal_map结构体向event_signal_map中添加event激活event_signal_map中的event删除event_signal_map中的event以下源码 ...

  6. libevent源码学习(5):TAILQ_QUEUE解析

    目录 前言 结点定义 链表初始化 链表查询及遍历 链表查询 链表遍历 插入结点 头插法 尾插法 前插法 后插法 删除结点 替换结点 总结 前言 在libevent中使用到了TAILQ数据结构,看了一下 ...

  7. libevent源码学习(9):事件event

    目录在event之前需要知道的event_baseevent结构体创建/注册一个event向event_base中添加一个event设置event的优先级激活一个event删除一个event获取指定e ...

  8. libevent源码学习(7):event_io_map

    event_io_map 哈希表操作函数 hashcode与equals函数 哈希表初始化 哈希表元素查找 哈希表扩容 哈希表元素插入 哈希表元素替换 哈希表元素删除 自定义条件删除元素 哈希表第一个 ...

  9. libevent源码学习(6):事件处理基础——event_base的创建

    目录前言创建默认的event_baseevent_base的配置event_config结构体创建自定义event_base--event_base_new_with_config禁用(避免使用)某一 ...

随机推荐

  1. 『与善仁』Appium基础 — 14、Appium测试环境搭建

    目录 1.Appium测试环境搭建整体思路 (1)Android测试环境搭建 (2)Appium测试环境搭建 (3)测试脚本语言的环境搭建 2.Appium在Android端和IOS端的工作流程 (1 ...

  2. 撸了一个可调试 gRPC 的 GUI 客户端

    前言 平时大家写完 gRPC 接口后是如何测试的?往往有以下几个方法: 写单测代码,自己模拟客户端测试. 可以搭一个 gRPC-Gateway 服务,这样就可以在 postman 中进行模拟. 但这两 ...

  3. [SDOI2012] Longge 的问题

    题意 求\(\sum_{i}^{n} gcd(i,n)\) 想法 套路题 \(\sum_{i}^{n} gcd(i,n)\) \(=\) \(\sum_{i,i | n} i * phi(n/i)\) ...

  4. DP 做题记录 II.

    里面会有一些数据结构优化 DP 的题目(如 XI.),以及普通 DP. *I. P3643 [APIO2016]划艇 题意简述:给出序列 \(a_i,b_i\),求出有多少序列 \(c_i\) 满足 ...

  5. android listview展示图片

    最近学习android开发,感触颇多,和网站开发对比,还是有很大的差距,在这里记录一下. android listview展示图片 在网站开发上,展示图片非常简单,一个HTML img标签就搞定,加上 ...

  6. 质量体系建设之路---可视化的MockServer

    一. 背景 福禄网络作为一家数字权益商品及服务提供商,覆盖了我们衣食住行的各种生活场景的权益内容,对接了如支付宝.京东.银行APP各种渠道,如何能够快速的响应渠道需求,提供稳定的接口服务,这就要求我们 ...

  7. Identity Server 4 从入门到落地(四)—— 创建Web Api

    前面的部分: Identity Server 4 从入门到落地(一)-- 从IdentityServer4.Admin开始 Identity Server 4 从入门到落地(二)-- 理解授权码模式 ...

  8. A Child's History of England.27

    Then, the Red King went over to Normandy, where the people suffered greatly under the loose rule of ...

  9. ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in..的错误 [转]

    问题: ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in..的错误 解决方法: 把没被singed的变量临时变更signed去处 ...

  10. 关于C语言中不同类型数据进行计算 有符号和无符号数进行计算

    float是8个有效位, 做个试验: 输出如下: 上面说明了什么: 1, 18/2.2 是除不尽的, 因为是define,所以没有给ratio变量赋值类型,但是从sizeof输出的结果是8,所以系统默 ...