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

轮中实线指针指向轮中的一个槽(slot)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动一次称为一个滴答(tick)。一个滴答的时间称为时间轮的槽间隔si(slot interval),它实际上就是定时器的心搏时间。该时间轮共有N个槽,因此它转一圈所需的时间就是N*si。每个槽中保存了一个定时器链表,时间轮的结构与哈希链表的结构是比较相似的。槽中的每条链表上的定时器具有相同的特征:它们的定时相差N*si的整数倍。时间轮正是利用这个关系将定时器三列到不同的链表中的。
假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts对应链表中的:
t s = ( c s + ( t i / s i ) ) % N ts = (cs + (ti / si)) \% N ts=(cs+(ti/si))%N
比如每个槽间隔si为100ms,N为600,转动一圈经过的时间为600*100ms,也就是60s。假设现在指针指向槽cs=10;我们添加一个定时时间60s也就是60000ms的定时器,
t s = ( 10 + ( 60000 / 100 ) ) % 600 = 10 ts = (10 + (60000 / 100)) \% 600 = 10 ts=(10+(60000/100))%600=10
也就是转一圈重新回到cs=10时将会触发,如果此时添加的定时时间为120s,也就是转两圈重新回到该位置时触发。因此需要一个变量来保存一个定时器需要转多少圈后才触发。
我们可以与I/O复用系统调用(select、poll、epoll)和时间轮一起来实现定时容器,将时间轮的的槽间隔作为I/O复用系统调用的超时值,当系统调用返回时,就检查当前指向的槽中的定时器,遍历槽中的定时器,如果圈数为0,则说明该定时器到期,执行相应的回调函数。如果圈数大于0,将其减一。遍历完成之后,将指针指向下一个槽并继续上述操作。
时间轮使用哈希表的思想,将定时器散列到不同的链表上,这样每条链表上的定时器就相对比较少。但是对时间轮而言,要提高精度,就要使si足够小。要提高执行效率,则要求N足够大。
2、代码实现如下:
该定时容器的思路是:将槽间隔作为I/O复用系统调用(select、poll、epoll)的超时值,当系统调用返回后就调用tick函数检查当前指针指向的槽的链表中的定时器,如果定时器中保存的圈数变量等于0,说明定时器到期,执行其中的回调函数,并删除该定时器。如果圈数大于0,说明该定时器还未到期,将圈数减一。遍历完成后,将指针指向下一个槽位。继续上述操作。
时间轮定时容器的几个接口介绍:
1) tick :在tick函数中循环查找定时器,如果定时器圈数为0,则定时器到期,执行其回调函数,然后删除该定时器。如果定时器圈数大于0,则将该变量减1。
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
time_wheel_timer.hpp
#ifndef _LIB_SRC_TIME_WHEEL_TIMER_H_
#define _LIB_SRC_TIME_WHEEL_TIMER_H_
#include "timer_common.hpp"
#include <array>
#include <list>
#include <iostream>
/*
* @Author: MGH
* @Date: 2021-09-29 12:57:45
* @Last Modified by: Author
* @Last Modified time: 2021-09-29 12:57:45
* @Description: Time Wheel Timer
*/
template <typename _UData>
class TimerNode
{
public:
TimerNode() = default;
~TimerNode() = default;
public:
void setTimeSlot(int slot)
{
this->_time_slot = slot;
}
int getTimeSlot()
{
return this->_time_slot;
}
void setRotation(int rotation)
{
this->_rotation = rotation;
}
int getRotation()
{
return this->_rotation;
}
public

最低0.47元/天 解锁文章
1153

被折叠的 条评论
为什么被折叠?



