定时器实现之最小堆(一)

1.概述

        定时器是一种用于在指定时间后执行特定任务或操作的机制。在计算机科学和编程中,定时器通常用于实现延时操作、周期性任务调度等功能。

         对于定时器的实现,核心分为两大块,分别是容器和触发方式,容器就是组织定时器中定时任务的数据结构,触发方式描述了我们以什么方式检测任务是否到期。今天的重点是定时器系列的开篇,能组织定时器的数据结构都要有一个基本的要求,就是能保证数据在数据结构内是有序的,通常的数据结构又最小堆、红黑树、时间轮等,本文将使用最小堆来组织定时器。

2.最小堆

        最小堆(Min-Heap)是一种特殊的完全二叉树,其中每个节点的值都小于或等于其子节点的值。这种特性使得最小堆的根节点总是存储着最小的元素。最小堆常用于实现需要快速访问最小元素的应用场景。 

        最小堆是一棵完全二叉树,这意味着除了最后一层外,其他所有层都是满的,并且最后一层的节点尽可能靠左排列。最小堆通常使用数组表示,其堆序性质:对于任意节点 ( i ),其子节点 ( 2i+1 ) 和 ( 2i+2 ) 满足以下条件:

  • heap[i] > heap[2i + 1]及父节点大于左子节点
  • heap[i] < heap[2i + 2]及父节点小于右子节点

3.代码实现 

         定义以下结构:

struct timer_entry {
    uint64_t time;            // 时间戳,用来表示定时任务的过期时间
    uint32_t min_heap_idx;    // 当前节点在堆(数组)中的索引
};

struct min_heap {
    timer_entry** p;
    uint32_t n; // n 为实际元素个数  
    uint32_t a; // a 为容量
};

         实现其核心调整算法:

#define min_heap_elem_greater(a, b) \
    ((a)->time > (b)->time)

static void min_heap_shift_up_(min_heap* s, uint32_t hole_index, timer_entry* e) {
    //计算父节点索引
    uint32_t parent = (hole_index - 1) / 2;
    //当前插入位置的索引不为0,并且插入位置的父节点的值比当前插入的值大,则交换位置知道这个条件不满足就把插入的元素插入到当前位置
    while (hole_index && min_heap_elem_greater(s->p[parent], e))
    {
        (s->p[hole_index] = s->p[parent])->min_heap_idx = hole_index;
        hole_index = parent;
        parent = (hole_index - 1) / 2;
    }
    (s->p[hole_index] = e)->min_heap_idx = hole_index;
}

static void min_heap_shift_down_(min_heap* s, uint32_t hole_index, timer_entry* e)
{
    // 计算右子节点的索引
    uint32_t 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]));
        // 如果最小的子节点还是小于尾节点则不调整直接交换,否则继续向下寻找
        if (!(min_heap_elem_greater(e, s->p[min_child]))) {
            break;
        }
            
        (s->p[hole_index] = s->p[min_child])->min_heap_idx = hole_index;
        hole_index = min_child;
        min_child = 2 * (hole_index + 1);
    }
    (s->p[hole_index] = e)->min_heap_idx = hole_index;
}

        最后对最小堆的一些行为进行封装实现,下面是完整代码:

//minheap.h

#pragma once

#include <cstdint>

struct timer_entry {
    uint64_t time;            // 时间戳,用来表示定时任务的过期时间
    uint32_t min_heap_idx;    // 当前节点在堆(数组)中的索引
};

struct min_heap {
    timer_entry** p;
    uint32_t n; // n 为实际元素个数  
    uint32_t a; // a 为容量
};

//初始化最小堆
int min_heap_init(min_heap** h, uint32_t a);

//销毁最小堆
int min_heap_free(min_heap* h);

//往最小堆中插入元素
int min_heap_push(min_heap* h, timer_entry* x);

//往最小堆中拿出最小元素
int min_heap_pop(min_heap* h, timer_entry** x);

//获取堆顶元素(不会从堆中删除)
int min_heap_top(min_heap* h, timer_entry** x);
// minheap.cpp

#include "minheap.h"

#include <cstdlib>

#define min_heap_elem_greater(a, b) \
    ((a)->time > (b)->time)

static void min_heap_shift_up_(min_heap* s, uint32_t hole_index, timer_entry* e) {
    //计算父节点索引
    uint32_t parent = (hole_index - 1) / 2;
    //当前插入位置的索引不为0,并且插入位置的父节点的值比当前插入的值大,则交换位置知道这个条件不满足就把插入的元素插入到当前位置
    while (hole_index && min_heap_elem_greater(s->p[parent], e))
    {
        (s->p[hole_index] = s->p[parent])->min_heap_idx = hole_index;
        hole_index = parent;
        parent = (hole_index - 1) / 2;
    }
    (s->p[hole_index] = e)->min_heap_idx = hole_index;
}

static void min_heap_shift_down_(min_heap* s, uint32_t hole_index, timer_entry* e)
{
    // 计算右子节点的索引
    uint32_t 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]);
        // 如果最小的子节点还是小于尾节点则不调整直接交换,否则继续向下寻找
        if (!(min_heap_elem_greater(e, s->p[min_child]))) {
            break;
        }
            
        (s->p[hole_index] = s->p[min_child])->min_heap_idx = hole_index;
        hole_index = min_child;
        min_child = 2 * (hole_index + 1);
    }
    (s->p[hole_index] = e)->min_heap_idx = hole_index;
}

// 堆大小动态增长
static int min_heap_reserve(min_heap* s, uint32_t n) {
    if (s->a < n)
    {
        timer_entry** p;
        unsigned a = s->a ? s->a * 2 : 8;
        if (a < n)
            a = n;
        if (!(p = (timer_entry**)realloc(s->p, a * sizeof(*p)))) {
            return -1;
        }
            
        s->p = p;
        s->a = a;
    }
    return 0;
}

int min_heap_init(min_heap** h, uint32_t a) {

    *h = (min_heap*)malloc(sizeof(min_heap));
    if(!*h) {
        return -1;
    }

    (*h)->p = (timer_entry**)malloc(a * sizeof(*((*h)->p)));
    if(!(*h)->p) {
        return -1;
    }

    (*h)->a = a;
    (*h)->n = 0;
    return 0;
}

int min_heap_free(min_heap* h) {
    if(h) {
        return -1;
    }

    if(h->p) {
        free(h->p);
    }

    free(h);
    return 0;
}

int min_heap_push(min_heap* h, timer_entry* x) {
    if(min_heap_reserve(h, h->n + 1)) {
        return -1;
    }

    min_heap_shift_up_(h, h->n++, x);
    return 0;
}

int min_heap_pop(min_heap* h, timer_entry** x) {
    if (h->n)
    {
        timer_entry* e = *(h->p);
        min_heap_shift_down_(h, 0, h->p[--h->n]);
        e->min_heap_idx = -1;
        *x = e;
        return 0;
    }
    return -1;
}

int min_heap_top(min_heap* h, timer_entry** x) {
    if (h->n)
    {
        timer_entry* e = *(h->p);
        *x = e;
        return 0;
    }
    return -1;
}

4.封装测试

        细心的同学可能注意到,上述最小堆里没有任何业务相关的东西,貌似就是一个什么都不做的最小堆,但看过linux内核的同学应该都知道我要怎么做了,话不多说上代码:

//main.cpp

#include "minheap.h"

#include <cstdint>
#include <iostream>
#include <functional>
#include <chrono>
#include <ctime>
#include <thread>

extern "C"{
#define offsetofs(s,m) (size_t)(&reinterpret_cast<const volatile char&>((((s*)0)->m)))
#define container_of(ptr, type, member) ({                  \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetofs(type, member) );})
}

using CallBack = std::function<void(void)>;

struct TimeNode {
    timer_entry env;
    CallBack cb;
};

class Timer {
public:
    Timer(uint32_t size);
    Timer() = delete;
    ~Timer();

    int addTimer(uint64_t time, CallBack cb);

    void run();

    void stop();

private:
    uint64_t GetNowTime();
private:
    min_heap* _heap;
    bool _close;
};

Timer::Timer(uint32_t size) {
    min_heap_init(&_heap, size);
    _close = false;
}

Timer::~Timer() {
    min_heap_free(_heap);
}

int Timer::addTimer(uint64_t time, CallBack cb) {
    TimeNode* node = new TimeNode();
    node->env.time = GetNowTime() + time;
    node->cb = cb;
    min_heap_push(_heap, &node->env);
    return 0;
}

uint64_t Timer::GetNowTime() {
    auto now = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
    uint64_t timestamp_ms = duration.count();
    return timestamp_ms;
}

void Timer::run() {
    while(!_close) {
        uint64_t now = GetNowTime();
        timer_entry* entry = nullptr;
        if(!min_heap_top(_heap, &entry)) {
            if(entry->time <= now) {
                if(!min_heap_pop(_heap, &entry)) {
                    TimeNode* node = container_of(entry, TimeNode, env);
                    if(node) {
                        node->cb();
                    }

                    delete node;
                }
            } else {
                std::this_thread::sleep_for(std::chrono::milliseconds(entry->time - now));
            }
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void Timer::stop() {
    _close = true;
}

int main(int, char**){

    Timer timer(10);

    auto now = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
    uint64_t timestamp_ms = duration.count();
    std::cout << "Start, now time :" << timestamp_ms << std::endl;
    timer.addTimer(1000, [&](void){
        auto now = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
        uint64_t timestamp_ms = duration.count();
        std::cout << "timer 1, now time :" << timestamp_ms << std::endl;
    });

    timer.addTimer(2000, [&](void){
        auto now = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
        uint64_t timestamp_ms = duration.count();
        std::cout << "timer 2, now time :" << timestamp_ms << std::endl;
    });

    std::thread t([&](){
        timer.run();
    });
    std::this_thread::sleep_for(std::chrono::milliseconds(3000));
    timer.stop();
    t.join();

    return 0;
}

         我的目标是实现一个通用定时器,里面包含所有定时器的实现方便选择,所以我使用了cmake加clangd加vscode来管理我的工程,目前我的目录结构如下:

         ok,现在编译运行看结果

学习参考:0voice · GitHub    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值