一、定时器
1、定时器是什么?
定时器不仅存在于硬件领域,在软件层面(客户端、网页和服务端)也普遍应用,核心功能都是高效管理大量延时任务。不同应用场景下,其实现方式和使用方法有所差异。
2、定时器解决了什么问题?
可以定期清理缓存,定时备份数据,在服务器负载较大时,自动执行一些重要的功能,提高服务器的效率和稳定性。
3、是怎么解决的?
- 组织大量延时任务的数据结构(容器)
- 触发最近将超时的任务的机制
4、实现方式:
- 对任务按触发时间进行排序:红黑树(map,set,multimap,multiset)–nginx,最小堆–libevent,libev,go,应用在单线程场景下
- 1、触发时刻作为key,任务作为val
- 2、快速找到最近要超时的任务
- 3、触发后要删除该任务且支持随时删除任务
- 4、允许相同时刻触发任务
- 对执行顺序进行组织:时间轮,针对当前时间指针做偏移。–netty,skynet,kafka,应用在多线程场景下
5、有哪些常用触发机制?
- I/O多路复用的最后一个超时参数
- 将定时器转化为io处理,timerfd
6、使用场景
- 与网络模块协同处理
- 基于事件驱动业务开展
- 除了协同网络处理,复用系统调用
二、具体实现
1、采用红黑树,对任务按触发时间进行排序
- map<key, value>: 以key存触发时间,value存任务,那么可能存在多个同一时刻的任务,不选
- multimap<key, value>:可能存储重复的值,然后操作起来比较麻烦,不选
- set: 不可能出现重复的,可以
那么以一个自定义结构作为key值进行存储,并且按触发时间进行排序
typedef struct TimerNode_S{
time_t expire;//过期时间
uint64_t id; //由于可能存在多个定时器在同一时间过期,所以需要一个唯一标识
}TimerNode_S;
bool operator < (const TimerNode_S& lhd, const TimerNode_S& rhd)
{
if(lhd.expire < rhd.expire){
return true;
}else if(lhd.expire > rhd.expire){
return false;
}else{ //如果相等,谁先插入,谁就先执行
return lhd.id < rhd.id;
}
}
set<TimerNode, less<>> timeouts;
2、计算最近触发的定时任务离当前还有多久?
time_t TimeOut()
{
auto iter = timeouts.begin();
if (iter == timeouts.end()) {
return -1;
}
time_t t = iter->expire - GetTick();
return t > 0 ? t : 0;
}
3、获取当前时间
/**
* @brief 获取当前时间的时间戳(以毫秒为单位)
*
* 使用 std::chrono::steady_clock 获取从系统启动到当前的时间
* std::chrono::system_clock,受系统时间影响,可能会被修改
*
* @return 返回当前时间的时间戳(以毫秒为单位)
*/
static inline time_t GetTick()
{
return chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
}
4、添加定时器
/**
* @brief 添加定时器
*
* 将一个定时器节点添加到定时器列表中,并返回该节点的标识。
*
* @param msec 定时器超时时间,单位为毫秒
* @param cb 定时器超时时执行的回调函数
*
* @return 返回定时器的标识,类型为 TimerNode_S
*/
TimerNode_S AddTimer(int msec, TimerNode::Callback cb)
{
time_t expire = GetTick() + msec; //过期时间
if(timeouts.empty() || expire <= timeouts.crbegin()->expire){
auto pairs = timeouts.emplace(GetID(), expire, move(cb));
return static_cast<TimerNode_S>(*pairs.first);
}
auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GetID(), expire, move(cb));
return static_cast<TimerNode_S>(*ele);
}
5、删除定时器
/**
* @brief 删除定时器节点
*
* 从定时器集合中删除指定的定时器节点。
*
* @param node 需要删除的定时器节点
*/
void DelTimer(TimerNode_S& node)
{
auto iter = timeouts.find(node);
if(iter != timeouts.end()){
timeouts.erase(iter);
}
}
6、处理定时器任务
/**
* @brief 处理超时事件
*
* 该函数遍历超时事件列表,对于已超时的每个事件,调用其回调函数进行处理,并从列表中移除该事件。
*
* @param now 当前时间戳
*/
void HandleTimeout(time_t now)
{
auto iter = timeouts.begin();
while(iter != timeouts.end() && iter->expire <= now){
iter->cb(*iter);
iter = timeouts.erase(iter);
}
}
struct epoll_event evs[64] = {0};
while(true){
int n = epoll_wait(epfd, evs, 64, timer->TimeOut());
time_t now = CTimer::GetTick();
for(int j = 0; j < n; ++j){
cout<<"epoll_wait:"<<endl;
}
timer->HandleTimeout(now);
}
以上是采用I/O多路复用的最后一个超时参数,接下来更换成timerfd
7、主要调用的函数
/*
*功能:创建定时器
*clockfd: CLOCK_REALTIME-系统实时时钟,与系统时间同步,受用户手动修改时间影响。CLOCK_MONOTONIC-单调递增时钟,自系统启动以来的时间,不受系统时间调整的影响
*flags:TFD_NONBLOCK(非阻塞模式)和 TFD_CLOEXEC(在 exec 调用时自动关闭文件描述符)
*
*/
timerfd_create(int clockfd, int flags);
/*
* 功能:用于设置定时器的初始超时时间和后续周期时间
* fd:timerfd
* flags: 控制定时器行为的标志:
* 0-------绝对时间
* TFD_TIMER_ABSTIME------表示相对时间
* new_value: 指定了定时器的初始超时时间和(可选的)后续周期时间
* old_value: 用于恢复定时器到之前的状态,常设为nullptr
*/
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
8、将之前的计算触发时间的方式改成timerfd
void UpdateTimerfd(const int fd) {
struct timespec abstime;
auto iter = timeouts.begin();
if (iter != timeouts.end()) {
abstime.tv_sec = iter->expire / 1000;
abstime.tv_nsec = (iter->expire % 1000) * 1000000;
} else {
abstime.tv_sec = 0;
abstime.tv_nsec = 0;
}
struct itimerspec its = {
.it_interval = {},
.it_value = abstime
};
timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);
}
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
struct epoll_event evs[64] = {0};
while(true){
timer->UpdateTimerfd(tfd);
int n = epoll_wait(epfd, evs, 64, -1);
time_t now = CTimer::GetTick();
for(int j = 0; j < n; ++j){
cout<<"epoll_wait:"<<endl;
}
timer->HandleTimeout(now);
}
三、总结
- 定时器在程序中无处不在,无论是硬件,还是网页,客户端,服务端等。
- 在服务端上合理地使用定时器,能提高服务器的效率和稳定性,如定时清理缓存,在服务器高负载情况下,自动执行一些重要的任务等。
- 定时器的数据结构多种多样,有根据触发时间排序的红黑树,最小堆,也有根据执行顺序的时间轮。
- 服务端常与网络模块协同处理
- 服务端常基于事件驱动业务开展
- 服务端除了协同网络处理,复用系统调用
代码:
Code
0voice·Github