当前位置: 首页 > news >正文

【C++实现】浅聊定时器的实现,最小堆配合map实现定时器

文章目录

  • 前言
    • 最小堆配合map实现定时器
    • 节点结构
    • 向上调整算法,向下调整算法
    • 插入操作
    • 删除任意元素操作
    • 处理过期时间


前言

定时器的使用:nginx,数据库的主从备份,心跳检测,游戏技能,武器的冷却,倒计时等等,其他需要使用超时机制的功能。
定时器主要用于需要使⽤超时机制的功能。
定时器的实现有两种方式:
第⼀种是,⽹络事件和时间事件在⼀个线程当中配合使⽤;例如nginx、redis;
第⼆种是,⽹络事件和时间事件在不同线程当中处理;例如skynet(轻量级的游戏开发框架)。

最小堆配合map实现定时器


这是一种最常见的实现方式。为什么需要map?因为删除堆当中任意一个元素的时间复杂度是O(N),因为删除之间需要查找,需要遍历整个数组查找该元素。而我们添加其他数据结构,如红黑树,哈希表来定位这个要删除的元素,删除操作的时间复杂度就能到O(logN)

  • map实现文件描述符到TimerNode的映射,后面简称Node了~
  • 最小堆当中存放的是TimerNode,堆内的比较方法是按照过期时间比较
    _map.insert(make_pair(id, node));

在不实现任意位置的删除操作,我们实际上不需要idx字段,但是由于心跳检测当中,倘若对方发送正常报文(而非心跳报文),我们依旧需要更新堆内部的过期时间。因为正常报文正常接受也能够说明这条链接依旧存活
idx能够在修改任意一个元素的时候O(1)时间获得删除元素的索引。并且删除同理。

修改堆内的对应的节点的场景:
在这里插入图片描述

删除堆顶元素的场景:
堆顶的时间超时了,可以认为连接已经无效了,此时堆顶的回调方法可以是 1.删除map当中文件描述符到节点的映射关系,2.删除epoll模型当中对应等待的文件描述符。3.close掉对应的文件描述符。
在这里插入图片描述

删除堆内任意元素的场景:
如epoll当中不需要关心该文件描述符了,此时需要堆中元素pop后,依旧是上面回调的三步骤。
在这里插入图片描述

增加堆内元素的场景:
如果epoll模型增加要关心的文件描述符,此时也需要将这个元素添加到堆当中,记录过期时间。此时需要push_back这个节点再向上调整,添加map当中文件描述符到节点的映射关系。
在这里插入图片描述

节点结构

typedef void (*TimerHandler) (struct TimerNode *node);

struct TimerNode {
    int idx = 0; // 元素在heap当中的位置,任意位置删除的时候用得到
    int id = 0;   //id 是 类的成员比变量,一直++,Count()函数获取。
    unsigned int expire = 0; // 过期时间
    TimerHandler cb = NULL; // 回调方法,当元素top出来后调用
};

向上调整算法,向下调整算法


以往写的两篇博客有叙述这块。相对简单,不做叙述了。
堆的任意位置的调整
【数据结构入门】从零实现–堆的实现

bool MinHeapTimer::_shiftDown(int pos){
    int last = (int)_heap.size()-1;
    int idx = pos;
    for (;;) {
        int left = 2 * idx + 1;
        if ((left >= last) || (left < 0)) {
            break;
        }
        int min = left; // left child
        int right = left + 1;
        if (right < last && !_lessThan(left, right)) {
            min = right; // right child
        }
        if (!_lessThan(min, idx)) {
            break;
        }
        std::swap(_heap[idx], _heap[min]);
        _heap[idx]->idx = idx;
        _heap[min]->idx = min;
        idx = min;
    }
    return idx > pos;
}

void MinHeapTimer::_shiftUp(int pos)
{
    for (;;) {
        int parent = (pos - 1) / 2; // parent node
        if (parent == pos || !_lessThan(pos, parent)) {
            break;
        }
        std::swap(_heap[parent], _heap[pos]);
        _heap[parent]->idx = parent;
        _heap[pos]->idx = pos;
        pos = parent;
    }
}

插入操作


  • 堆当中的timeout时间是当前时间 + expire时间
  • 也就是尾部进行一次向上调整。
//expire 表示距离现在的过期时间
//cb 是回调方法,只有pop的时候才会调用。
int MinHeapTimer::AddTimer(uint32_t expire, TimerHandler cb) {
    int64_t timeout = current_time() + expire;
    TimerNode* node = new TimerNode;
    int id = Count();
    node->id = id;
    node->expire = timeout;
    node->cb = cb;
    node->idx = (int)_heap.size();
    _heap.push_back(node);
    _shiftUp((int)_heap.size() - 1);
    _map.insert(make_pair(id, node));
    return id;
}

删除任意元素操作


删除任意一个位置的元素,给定参数id,从map当中找到对应的节点,删除该节点。

bool MinHeapTimer::DelTimer(int id)
{
	//id  -> Node
    auto iter = _map.find(id);
    if (iter == _map.end())
        return false;
    _delNode(iter->second);// 删除Node
    return true;
}
void MinHeapTimer::_delNode(TimerNode *node)
{
	//节点所在位置为idx, idx的元素与末尾元素交换,再进行一次向上或向下调整即可。
    int last = (int)_heap.size() - 1;
    int idx = node->idx;
    if (idx != last) {
        std::swap(_heap[idx], _heap[last]);
        _heap[idx]->idx = idx;
        if (!_shiftDown(idx)) {
            _shiftUp(idx);
        }
    }
    _heap.pop_back();//从堆内删除该元素
    _map.erase(node->id);// 从_map删除该映射
    delete node;
}

处理过期时间


从堆顶获取元素,检测时间是否过期,过期就删除,直到堆内无数据或者堆顶元素没有超时。这里实际上属于业务上处理,若是心跳检测的程序,那么此时就需要断开与对端的连接。
只有这里会调用已经注册的回调函数。

void MinHeapTimer::ExpireTimer()
{
    if (_heap.empty()) return;
    uint32_t now = current_time();
    do {
        TimerNode* node = _heap.front();
        if (now < node->expire)//堆顶元素没过期
            break;
        if (node->cb) {//若有注册回调函数,则调用。
            node->cb(node);
        }
        _delNode(node);
    } while(!_heap.empty());//堆内无元素就退出
}

相关文章:

  • Spring五大类注解读取存储Bean对象
  • 数据备份管理中的分类定级:方法、标准与策略
  • 一次日常需求处理带给我的思考
  • 2022年PMP考试换大纲了,但是PMBOK还没更新,该如何准备?
  • 专业五月考自测
  • js之求最值的三种方法——Math.min()和 Math.max()、最小值array.sort()[0]、Math.min(...[v1, v2...])
  • springboot毕设项目易捷接待系统761z7(java+VUE+Mybatis+Maven+Mysql)
  • jar包,引入依赖
  • 最大似然估计(MLE)入门教程
  • 【leetcode】【2022/9/3】646. 最长数对链
  • Matlab:Matlab编程语言应用之数学计算(向量数组矩阵索引、矩阵索引四则运算、行列式与线性系统求解)的简介、案例实现之详细攻略
  • c++11 多线程支持 (std::async)
  • 修复 JavaScript 错误的四种方法
  • 基本的Python内置函数
  • WANem弱网环境模拟工具的使用探索
  • SegmentFault for Android 3.0 发布
  • #Java异常处理
  • 2017年终总结、随想
  • angular组件开发
  • hadoop集群管理系统搭建规划说明
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • JS学习笔记——闭包
  • MySQL用户中的%到底包不包括localhost?
  • MySQL主从复制读写分离及奇怪的问题
  • Odoo domain写法及运用
  • PhantomJS 安装
  • vuex 学习笔记 01
  • WinRAR存在严重的安全漏洞影响5亿用户
  • 彻底搞懂浏览器Event-loop
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 关于字符编码你应该知道的事情
  • 回流、重绘及其优化
  • 设计模式走一遍---观察者模式
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 项目管理碎碎念系列之一:干系人管理
  • 消息队列系列二(IOT中消息队列的应用)
  • 一、python与pycharm的安装
  • 用 Swift 编写面向协议的视图
  • 白色的风信子
  • 容器镜像
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • ​LeetCode解法汇总307. 区域和检索 - 数组可修改
  • ​secrets --- 生成管理密码的安全随机数​
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • $.ajax()参数及用法
  • $NOIp2018$劝退记
  • (13):Silverlight 2 数据与通信之WebRequest
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (定时器/计数器)中断系统(详解与使用)
  • (分布式缓存)Redis哨兵
  • (四)Tiki-taka算法(TTA)求解无人机三维路径规划研究(MATLAB)
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)Unity3DUnity3D在android下调试
  • (转)总结使用Unity 3D优化游戏运行性能的经验
  • (转载)Linux 多线程条件变量同步