在网络程序中我们通常要处理三种事件,网络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

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

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



