服务端定时器的学习(一)

发布于:2025-06-04 ⋅ 阅读:(23) ⋅ 点赞:(0)

一、定时器

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


网站公告

今日签到

点亮在社区的每一天
去签到