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. Codeforces 516E - Drazil and His Happy Friends(同余最短路)

    Codeforces 题面传送门 & 洛谷题面传送门 首先思考一个非常简单的性质:记 \(d=\gcd(n,m)\),那么每次在一起吃完饭的男女孩编号必定与 \(d\) 同余,而根据斐蜀定理可 ...

  2. SSRF的原理和防范

    背景 最近做的安全测评主要是SSRF,发现自己在这一块有挺大知识盲点,抓紧补一下. 1.介绍 SSRF(Server-Side Request Forgery:服务器端请求伪造),是一种攻击者利用服务 ...

  3. Linux—ps -ef 命令输出信息的具体含义(显示所有正在运行的命令程序)

    linux 中使用 ps -ef 输出参数的具体含义 功能:显示所有正在运行的命令程序 UID: 说明该程序被谁拥有PID:就是指该程序的 IDPPID: 就是指该程序父级程序的 IDC: 指的是 C ...

  4. List 去重的 6 种方法,这个方法最完美!

    在日常的业务开发中,偶尔会遇到需要将 List 集合中的重复数据去除掉的场景.这个时候可能有同学会问:为什么不直接使用 Set 或者 LinkedHashSet 呢?这样不就没有重复数据的问题了嘛? ...

  5. MySQL8.0配置文件详解

    mysql8.0配置文件一.关键配置1. 配置文件的位置 MySQL配置文件 /etc/my.cnf 或者 /etc/my.cnf.d/server.cnf几个关键的文件:.pid文件,记录了进程id ...

  6. idea Error : java 不支持发行版本5

    问题描述 在Intellij idea中新建了一个Maven项目,运行时报错如下:Error : java 不支持发行版本5 解决 1.在Intellij中点击"File" --& ...

  7. 面对大规模 K8s 集群,这款诊断利器必须要“粉一波”!

    作者|段超 来源|尔达 Erda 公众号 背景 我们是一家做商业软件的公司,从一开始我们就把软件交付流程做的非常标准且简单,所有的软件都是基于我们的企业数字化平台 Erda(现已开源)来交付,底层基于 ...

  8. Learning Spark中文版--第四章--使用键值对(1)

      本章介绍了如何使用键值对RDD,Spark中很多操作都基于此数据类型.键值对RDD通常在聚合操作中使用,而且我们经常做一些初始的ETL(extract(提取),transform(转换)和load ...

  9. 零基础学习java------day18------properties集合,多线程(线程和进程,多线程的实现,线程中的方法,线程的声明周期,线程安全问题,wait/notify.notifyAll,死锁,线程池),

    1.Properties集合 1.1 概述: Properties类表示了一个持久的属性集.Properties可保存在流中或从流中加载.属性列表中每个键及其对应值都是一个字符串 一个属性列表可包含另 ...

  10. nodejs-npm模块管理器

    JavaScript 标准参考教程(alpha) 草稿二:Node.js npm模块管理器 GitHub TOP npm模块管理器 来自<JavaScript 标准参考教程(alpha)> ...