【C++实现】浅聊定时器的实现,最小堆配合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());//堆内无元素就退出
}
  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
### 回答1: 高精度定时器是指能够实现毫秒级别或更高精度的定时器。在MFC中实现高精度定时器可以通过以下步骤实现: 1.创建一个窗口,并通过SetTimer函数设置一个定时器。该函数有两个参数,一个是定时器ID号,一个是定时器的时间间隔。 2.在窗口类中添加一个成员变量,用于保存已经过去的时间。我们可以在WM_TIMER消息处理函数中,每次处理时递增该变量。 3.通过该变量计算出所需的时间,然后进行相应的操作。例如,我们可以将该变量转化为分钟和秒钟,然后在窗口上显示出来。 4.为了提高定时器的精度,可以通过Win32 API函数timeGetTime获取系统时间,然后在WM_TIMER消息处理函数中计算与上一次时间间隔,从而更加精确地计算已经过去的时间。 需要注意的是,高精度定时器会占用系统资源,并且可能存在时间误差。因此,在实现时需要考虑这些因素,并根据实际需求进行调整。 ### 回答2: 高精度定时器是一种能够实现较为精确的时间计量和延时控制的技术,而MFC(Microsoft Foundation Classes)则是基于Windows操作系统的C++类库,提供了GUI界面开发所需要的各种类、函数和控件等工具。将两者结合使用,可以实现用MFC编写的应用程序对时间的更加准确的控制或监测,如毫秒或微秒级别的时间计算和处理等。 要在MFC中实现高精度定时器功能,可以考虑使用Win32 API中提供的计时器函数来进行实现。具体实现步骤如下: 1. 定义计时器变量和时间变量。例如: UINT_PTR m_TimerID; // 计时器ID DWORD m_dwStartTime; // 记录开始时间 DWORD m_dwCurrentTime; // 记录当前时间 DWORD m_dwElapsedTime; // 记录已过时间 2. 创建计时器并开始计时。可以在窗口初始化函数中添加如下代码: m_TimerID = SetTimer(1, 1, NULL); // 1ms间隔 m_dwStartTime = GetTickCount(); // 记录开始时间 3. 处理计时器消息。在窗口消息响应函数中,添加对WM_TIMER消息的处理,如: case WM_TIMER: { m_dwCurrentTime = GetTickCount(); // 记录当前时间 m_dwElapsedTime = m_dwCurrentTime - m_dwStartTime; // 计算已过时间 // 这里可以根据需要进行时间数据的显示、处理等其他操作 } break; 4. 在窗口关闭时停止计时器。可以在窗口关闭函数中添加如下代码: KillTimer(m_TimerID); 以上就是使用高精度定时器实现MFC的简单示例。需要注意的是,由于不同计算机的性能和Windows操作系统的版本等因素可能会影响计时器的精度和稳定性,因此在实际应用中需要针对具体需求进行测试和调整。 ### 回答3: 高精度定时器可以通过MFC的计时器来实现。MFC的计时器是基于Windows API的定时器实现的。Windows API提供了一个SetTimer函数,用于设置定时器。MFC的CWnd类继承了Windows API的CWnd类,在此基础上提供了一系列的计时器函数。 使用MFC计时器,首先需要在类声明中添加一个计时器ID,具体实现可以如下: #define TIMER_ID 1 class CMyDlg : public CDialog { public: CMyDlg(CWnd* pParent = NULL); // 对话框数据 #ifdef AFX_DESIGN_TIME enum { IDD = IDD_MYDLG_DIALOG }; #endif protected: virtual void DoDataExchange(CDataExchange* pDX); protected: HICON m_hIcon; int m_nCount; afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnBnClickedButtonStart(); afx_msg void OnBnClickedButtonStop(); DECLARE_MESSAGE_MAP() }; 在类声明中添加了一个计时器ID为1。同时,在消息映射中,添加了一个响应定时器事件的函数OnTimer。 void CMyDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == TIMER_ID) { m_nCount++; //每次增加计数 } CDialog::OnTimer(nIDEvent); } OnTimer函数响应计时器事件,其中nIDEvent就是计时器ID。在函数中,我们可以编写计时器事件响应的代码,这里是每次增加计数。 在对话框初始化时就设置计时器: BOOL CMyDlg::OnInitDialog() { CDialog::OnInitDialog(); // 将“关于...”菜单项添加到系统菜单中。 // IDM_ABOUTBOX 必须在系统命令范围内。 ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { BOOL bNameValid; CString strAboutMenu; bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX); ASSERT(bNameValid); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动 // 执行此操作 SetIcon(m_hIcon, TRUE); // 设置大图标 SetIcon(m_hIcon, FALSE); // 设置小图标 // TODO: 在此添加额外的初始化代码 SetTimer(TIMER_ID, 500, NULL); return TRUE; // 除非将焦点设置到控件,否则返回 TRUE } 在OnInitDialog函数中添加代码SetTimer(TIMER_ID, 500, NULL);就可以设置一个500ms的计时器了。 当然,在对话框关闭时,还要记得取消计时器: void CMyDlg::OnBnClickedButtonStop() { // TODO: 在此添加控件通知处理程序代码 KillTimer(TIMER_ID); } 这样,一个MFC的高精度定时器实现了。其实,在MFC中,还可以使用CWnd::SetTimer来设置定时器,不过与SetTimer函数类似,使用时也需要取消,不然会造成内存泄漏。同时,MFC还提供了更为灵活的计时器功能,可以用来处理其他复杂的问题。
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

^jhao^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值