定时器方案 红黑树、时间轮学习笔记

目录

一、定时器应用

二、定时器在工作中的场景

三、定时器的触发方式

 1.网络事件和定时事件在一个线程中处理

2.网络事件和定时事件在不同线程中处理

四、定时器设计

1.接口设计

2.数据结构设计

五、红黑树

六、时间轮

1.从时钟运行看时间轮的原理

2.时间轮的使用场景

3.设计时间轮


一、定时器应用

定时器属于基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持。常见的应用有:

  • 网络编程中的心跳检测
  • 游戏编程中的技能冷却
  • 倒计时

总之,定时器是用来处理延时任务,即间隔多少秒后触发某个任务。

二、定时器在工作中的场景

定时器在工作中的场景,常见的有以下几种:

  • 面试——手写定时器
  • 为项目添加或替换定时器模块
  • 使用定时器

三、定时器的触发方式

 1.网络事件和定时事件在一个线程中处理

这种情况下需要协同处理,即将定时事件杂糅到网络事件中一起进行处理。

为什么可以协同处理?

reactor是基于事件的网络模型,其IO处理是同步的,事件处理是异步的(因为注册事件和处理事件是在两个流程中,所以是异步的)。我们的定时任务的处理也是异步的,即我们可以使用reactor进行协同处理。

怎么协同处理?

利用IO多路复用“阻塞”收集就绪事件的接口,如epoll_wait/select/poll的最后一个参数timeout进行协同处理。

有哪些场景?

  • 单reactor—— redis(可以跟踪源码查看)
  • 多reactor—— memcached、nginx

有什么特征?

  • 定时任务比较少(如nginx每个进程默认配置下最多有1024个定时器)
  • 对于多线程情况,容易引起事件处理不均衡
  • 对于单线程,任务多了会影响网络事件的处理

2.网络事件和定时事件在不同线程中处理

对于此种触发方式,定时任务在单独的线程中检测,通常处理大量的定时任务。

那么它是怎么进行处理的呢?

  • .用usleep(time)触发, time要小于最小时间精度
  • 通常采用的数据结构是时间轮,其优点为加锁粒度比较少。时间轮只负责检测,通常通过信号或者插入执行队列让其他线程执行。

四、定时器设计

1.接口设计

// 初始化定时器
void init_timer();
// 添加定时器
Node* add_timer(int expire, callback cb);
// 删除定时器
bool del_timer(Node* node);
// 找到最近要触发的定时任务
Node* find_nearest_timer();
// 更新检测定时器
void update_timer();
// 清除定时器
// void clear_timer();

其中,find_nearest_timer()只在第一种触发方式中使用。

2.数据结构设计

本质:按照定时任务的优先级进行组织,谁先执行谁的优先级就更高。定时器的组织方式有两类:

  • 可以按照触发时间顺序进行组织,如红黑树(绝对有序,如Nginx)、最小堆(相对有序,如libevent、libev等)、跳表(绝对有序,redis未来会引用跳表)等数据结构。特别的需要考虑相同时间触发的定时任务。
  • 按执行顺序组织,如用时间轮。

五、红黑树

我们可以使用红黑树来实现定时器,其使用的触发方式为网络事件和定时事件在一个线程中处理。C++中实现了红黑树的容器有set、multiset、map和multimap等。此处我们使用set容器来实现定时器。

对于相同触发时间的定时任务,越后插入的,插入位置放在红黑树越右侧,这样可以保证后面插入的后执行。定时器实现代码参考如下:

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

/*此结构用来唯一标识一个定时节点*/
struct TimerNodeBase {
    time_t expire;//触发时间
    int64_t id;//用来描述插入先后顺序
};

struct TimerNode : public TimerNodeBase {
    using Callback = std::function;
    Callback func;
    TimerNode(int64_t id, time_t expire, Callback func) : func(func) {
        this->expire = expire;
        this->id = id;
    }
};

/*比较仿函数,使用基类引用多态特性*/
bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) {
    if (lhd.expire < rhd.expire)
        return true;
    else if (lhd.expire > rhd.expire) 
        return false;
    return lhd.id < rhd.id;
}

class Timer {
public:
/*
steady_clock 是单调的时钟,表示系统启动到当前的时间;只会增长,适合用于记录程序耗时;
system_clock 是系统的时钟;因为系统的时钟可以修改;甚至可以网络对时; 所以用系统时间计算时间差可能不准。
high_resolution_clock 是当前系统能够提供的最高精度的时钟;它也是不可以修改的。相当于 steady_clock 的高精度版本。
*/
    static time_t GetTick() {
        auto sc = chrono::time_point_cast(chrono::steady_clock::now());
        auto temp = chrono::duration_cast(sc.time_since_epoch());
        return temp.count();
    }

    TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func) {
        time_t expire = GetTick() + msec;
        auto ele = timermap.emplace(GenID(), expire, func);//emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。
        return static_cast(*ele.first);
    }
    
    bool DelTimer(TimerNodeBase &node) {
        auto iter = timermap.find(node);
        if (iter != timermap.end()) {
            timermap.erase(iter);
            return true;
        }
        return false;
    }

    bool CheckTimer() {
        auto iter = timermap.begin();
        if (iter != timermap.end() && iter->expire <= GetTick()) {
            iter->func(*iter);
            timermap.erase(iter);
            return true;
        }
        return false;
    }

    time_t TimeToSleep() {
        auto iter = timermap.begin();
        if (iter == timermap.end()) {
            return -1;
        }
        time_t diss = iter->expire-GetTick();
        return diss > 0 ? diss : 0;
    }
private:
    static int64_t GenID() {
        return gid++;
    }
    static int64_t gid;
    set> timermap;
};
int64_t Timer::gid = 0;


int main() {
    int epfd = epoll_create(1);

    unique_ptr timer = make_unique();

    int i =0;
    timer->AddTimer(1000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << "node id:" << node.id << " revoked times:" << ++i << endl;
    });

    timer->AddTimer(1000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << "node id:" << node.id << " revoked times:" << ++i << endl;
    });

    timer->AddTimer(3000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << "node id:" << node.id << " revoked times:" << ++i << endl;
    });

    auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
        cout << Timer::GetTick() << "node id:" << node.id << " revoked times:" << ++i << endl;
    });

    timer->DelTimer(node);

    cout << "now time:" << Timer::GetTick() << endl;
    epoll_event ev[64] = {0};

    while (true) {
        /*
            -1 永久阻塞
            0 没有事件立刻返回,有事件就拷贝到 ev 数组当中
            t > 0  阻塞等待 t ms, 
            timeout  最近触发的定时任务离当前的时间
        */
        int n = epoll_wait(epfd, ev, 64, timer->TimeToSleep());
        for (int i = 0; i < n; i++) {
            /**/
        }
        /* 处理定时事件*/
        while(timer->CheckTimer());
    }
    
    return 0;
}

六、时间轮

1.从时钟运行看时间轮的原理

定时器方案 红黑树、时间轮学习笔记_第1张图片

时间轮的实现要重点关注两点:时间精度(1s)和时间范围(12)。 

时间轮为什么要分成多个层级?

  • 减少空间占用(不需要12*60*60个存储单位,只需要12+60+60个存储单位)
  • 只需关注最近要触发的定时任务(最近一分钟内的任务)
  • 按照任务触发的轻重缓急进行组织
  • 减少任务检测

我们用tick表示当前时间,其取值范围为时间范围,我们只需要记录一个指针。当秒针移动一圈,说明下一分钟的任务快执行了。当分钟移动一圈,说明下一小时的任务快执行了。

任务节点需要包括expire(定时器触发时间)、callback(回调函数)和next(指向相同触发时间的任务)三个字段。

添加节点时需要根据time判断放在哪一层,并通过expire=time+tick0计算超时时间。

 当下一分钟是任务快执行时,需要重新进行映射。

为什么需要重新映射?因为时间精度为秒,只执行秒针层的任务。分针层和时针层的任务如果需要执行都需要向上一层级重新映射。

怎么重新映射?

  • 确定重新映射位置(算出分针指针的位置),即 (tick/60)%60
  • 取出该分针指针指向槽位所有任务
  • 重新计算每个定时任务的超时时间,即time=time+tick0-tick
  • 重新添加节点逻辑

对于删除节点,我们可以怎么做呢?由于存在重新映射,节点的位置可能发生变化。所以我们不能直接删除节点。但是我们可以添加一个字段cancel,并赋值为true。当任务触发时,遇到这个标记就不执行具体任务。

2.时间轮的使用场景

包括内核、skynet、kafka和netty等。

3.设计时间轮

需要考虑如下因素:

  • 确定时间范围
  • 确定时间精度——由usleep和gettime这两个接口确定
  • 确定时间层级——第一层级组织最近关注的延时任务,第一层级是实际执行层级,其他层级只负责向上一层级重新映射
  • 实现添加节点接口
  • 实现重新映射

 本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接,详细查看详细的服务:

服务器高级架构体系:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

你可能感兴趣的:(服务器,linux,c++)