高性能定时器1——最小堆实现

本文详细介绍了如何使用Linux下的I/O复用系统调用(select/poll/epoll)配合最小堆数据结构实现高效的定时器容器,以管理服务器的定时事件,确保在预期时间触发且不影响核心逻辑。通过实例演示了如何在回射服务器中应用此技术,以及如何通过epoll统一处理I/O和定时事件。

​ 在网络程序中我们通常要处理三种事件,网络I/O事件、信号以及定时事件,我们可以使用I/O复用系统调用(select、poll、epoll)将这三类事件进行统一处理。我们通常使用定时器来检测一个客户端的活动状态,服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此我们需要将每个定时事件分别封装为定时器,并使用某种容器类数据结构,比如:链表、排序链表、最小堆、红黑树以及时间轮等,将所有定时器串联起来,以实现对定时事件的统一管理。此处所说的定时器,确切的说应该是定时容器,定时器容器是容器类数据结构;定时器则是容器内容纳的一个个对象,它是对定时事件的封装,定时容器是用来管理定时器的。


在本文中将主要介绍使用最小堆来实现的定时容器。

1、I/O复用系统调用的超时参数

​ Linux下的3组I/O复用系统调用(select、poll、epoll)都带有定时参数,因此他们不仅能统一处理信号和I/O事件,也能统一处理定时事件。我们可以使用定时容器和I/O复用系统调用来共同实现定时器的触发。

这三个系统调用的定义如下:

#include <sys/select.h>
int select(int fds, fd_set *readfds, fd_set *writerfds, fd_set *exceptfds, struct timeval *timeout);
struct timeval
{
   
   
    long tv_sec;        //秒数
    long tv_usec;       //微妙数
}

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

这三个系统调用都有一个timeout的参数,当发生I/O事件时,这三个系统调用将会返回;当指定的时间到达时,如果没有I/O事件发生,这三个系统调用也会返回。

2、时间堆定时容器

​ 该定时器容器的思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔,这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再从剩余的定时器中找到超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔,如此反复,就实现了较为精确的定时。

​ 最小堆很适合处理这种定时方案。最小堆是指每个节点的值都小于或等于其子节点的值的完全二叉树。

​ 树的基本操作是插入和删除节点。对最小堆而言,它们都很简单。为了将一个元素X插入最小堆,我们可以在树的下一个空闲位置创建一个空穴,如果X可以放在空穴中而不破坏堆序,则插入完成。否则就执行上滤操作,即交换空穴和它的父节点上的元素,不断执行上述过程,直至X可以放入空穴,则插入操作完成。可以按照下图的步骤来操作,假设要在最小堆中插入一个元素14:

在这里插入图片描述

​ 最小堆的删除操作指的是删除其根节点上的元素,并且不破坏堆序性质。执行删除顶部元素操作时,我们需要先在根节点处创建一个空穴,由于堆现在少了一个元素,因此我们可以将堆的最后一个元素X移动到该堆的某个地方。如果X可以被放入空穴,则删除操作完成。否则就执行下滤操作,即交互空穴和它的两个儿子中的较小者。不断进行上述过程,直至X可以被放入空穴,则删除操作完成。比如我们对上图插入元素前的最小堆执行删除顶部元素操作:

在这里插入图片描述

​ 由于最小堆是一种完全二叉树,所以我们可以使用数组来组织其中的元素。对于数组中的任意一个位置i上的元素,其左儿子节点在位置2i+1上,右儿子在位置2i+2上,其父节点在位置[(i - 1) / 2](i>0)上。与用链表来表示堆相比,用数组不仅节省空间,而且更容易实现堆的插入、删除等操作。

最小堆实现的定时容器的代码实现如下:

​ 在timer_common.hpp中定义了Timer以及ITimerContainer两个类,Timer类为定时器类,ITimerContiner类为定时器容器的一个虚基类或者说是接口,后续将实现最小堆定时器、时间轮定时器以及红黑树定时器,这几个定时器都实现了ITimerContainer中的方法。
最小堆定时容器的几个接口介绍:

​ 1) tick :在tick函数中循环查找超时值最小的定时器,并调用其回调函数,直到找到的定时器的没有超时,就结束循环。

​ 2)addTimer::向容器中添加一个定时器,并返回定时器的指针。

​ 3)delTimer::根据传入的定时器指针删除容器中的一个定时器,并且销毁资源。

​ 4)resetTimer: 重置一个定时器。

​ 5)getMinExpire:获取容器中超时值最小的绝对时间;

timer_common.hpp:

#ifndef _LIB_SRC_TIMER_COMMON_H
#define _LIB_SRC_TIMER_COMMON_H

#include <stdio.h>
#include <sys/time.h>

// 获取时间戳 单位:毫秒
time_t getMSec()
{
   
   
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// 定时器数据结构的定义
template <typename _User_Data>
class Timer
{
   
   
public:
    Timer() : _user_data(nullptr), _cb_func(nullptr) {
   
   };
    Timer(int msec) : _user_data(nullptr), _cb_func(nullptr)
    {
   
   
        this->_expire = getMSec() + msec;
    }

    ~Timer()
    {
   
   

    }

    void setTimeout(time_t timeout)
    {
   
   
        this->_expire = getMSec() + timeout;
    }

    time_t getExpire()
    {
   
   
        return _expire;
    }

    void setUserData(_User_Data *userData)
    {
   
   
        this->_user_data = userData;
    }

    void handleTimeOut()
    {
   
   
        if(_cb_func)
        {
   
   
            _cb_func(_user_data);
        }
        
    }

    using TimeOutCbFunc = void (*)(_User_Data *);
    void setCallBack(TimeOutCbFunc callBack)
    {
   
   
        this->_cb_func = callBack;
    }

private:
    time_t _expire;                    // 定时器生效的绝对时间            
    _User_Data *_user_data;            // 用户数据
    TimeOutCbFunc _cb_func;           // 超时时的回调函数
};



template <typename _UData>
class ITimerContainer 
{
   
   
public:
    ITimerContainer() = default;
    virtual ~ITimerContainer() = default;

public:
    virtual void tick() = 0;               
    virtual Timer<_UData> *addTimer(time_t timeout) = 0;
    virtual void delTimer(Timer<_UData> *timer) = 0;
    virtual void resetTimer(Timer<_UData> *timer, time_t timeout) = 0;
    virtual int getMinExpire() = 0;
};

#endif

heap_timer.hpp:

#ifndef _LIB_SRC_HEAP_TIMER_H_
#define _LIB_SRC_HEAP_TIMER_H_


#include <iostream>
#include "timer_common.hpp"


/*
 * @Author: MGH
 * @Date: 2021-09-29 13:01:04
 * @Last Modified by: Author
 * @Last Modified time: 2021-09-29 13:01:04
 * @Description: Heap Timer
*/

#define HEAP_DEFAULT_SIZE 128



// 定时器数据结构的定义
template <typename _User_Data>
class HeapTimer
{
   
   
public:
    HeapTimer() = default;
    HeapTimer(int msec)
    {
   
   
        timer.setTimeout(msec);
    }

    ~HeapTimer()
    {
   
   

    }

    void setTimeout(time_t timeout)
    {
   
   
        timer.setTimeout(timeout);
    }

    time_t getExpire()
    {
   
   
        return timer.getExpire();
    }

    void setUserData(_User_Data *userData)
    {
   
   
        timer.setUserData(userData);
    }

    int getPos()
    {
   
   
        return _pos;
    }

    void setPos(int pos)
    {
   
   
        this->_pos = pos;
    }

    void handleTimeOut()
    {
   
   
        timer.handleTimeOut();    
    }

    using TimeOutCbFunc = void (*)(_User_Data *);
    void setCallBack(TimeOutCbFunc callBack)
    {
   
   
        timer.setCallBack(callBack);
    }

public:
    Timer<_User_Data> timer;  

private:
      
    int _pos;                          // 保存该定时器在数组中的位置,以便查找删除操作            
};


// 定时容器,使用最小堆实现
template <typename _UData>
class HeapTimerContainer : public ITimerContainer<_UData> 
{
   
   
public
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值