时间轮定时器的实现(C++)

时间轮定时器的实现(C++)

写在前面(个人)

  本文主要介绍下时间轮定时器的实现,《Linux高性能服务器编程》书中第11章内容。
  该书我已上传到CSND:https://download.csdn.net/download/qq_34039018/12035959

  在学习了升序定时器链表后,其实对定时器的大概思想就算有初步的理解了,包括后面的最小堆定时器也只是进行优化。
  回到时间轮定时器,它主要解决的是升序链表插入效率比较低的问题,根据相关链表算法的理论,因为在有序链表插入节点的时间复杂度为O(n),而且是单链表,意味着链表越长,插一个节点所要找到合适位置的时间开销就会越大,这样下来,时间效率是比较低的。
  时间轮定时器算法有点hash的思想,插入节点是采用【取模+头插法】的方式,将插入的平均时间复杂度控制器到了O(1),极大节省了时间开销。
  不过呢,升序链表定时器虽然在插入时间复杂度上为O(n),但是在处理超时定时器时遍历链表的效率还是要比时间轮定时器好的,时间轮定时器需要将对应槽上的链表从头到尾全部判断一次,而链表则是从头开始处理,一旦遇到未超时的,则直接结束遍历就好。但综合效率来说,时间轮还是比升序链表好很多的。
  下面的为书上的内容。

介绍

  基于排序链表的定时器存在一个问题:添加定时器的效率偏低。一种简单的时间轮如图所示:
时间轮定时器的实现(C++)_第1张图片
  在这个时间轮中,实线指针指向轮子上的一个槽(slot)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(slot)。每次转动称为一个滴答(tick)。一个tick时间间隔为时间轮的si(slot interval)。该时间轮共有N个槽,因此它转动一周的时间是Nsi.每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差Nsi的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timer slot)对应的链表中:

ts=(cs+(ti/si))%N

  基于排序链表的定时器使用唯一的一条链表来管理所有的定时器,所以插入操作的效率随着定时器的数目增多而降低。而时间轮使用了哈希表处理冲突的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。

  很显然,对于时间轮而言,要提高精度,就要使si的值足够小; 要提高执行效率,则要求N值足够大,使定时器尽可能的分布在不同的槽。

  下列代码描述了一个简单的时间轮,如果想继续提高效率,可以实现多个不同精度的轮子,提高查找的效率。

程序实现

  • tw_timer.h
//
// Created by yongpu on 2019/12/15.
//

#ifndef TIMER_WHEEL_TW_TIMER_H
#define TIMER_WHEEL_TW_TIMER_H

#include 
#include 
#include 

#define BUFFER_SIZE 64

class tw_timer;

/* 绑定socket和定时器 */
struct client_data {
    sockaddr_in address;
    int sockfd;
    char buf[BUFFER_SIZE];
    tw_timer *timer;
};

/* 定时器类 */
class tw_timer {
public:
    tw_timer(int rot, int ts) {
        next = nullptr;
        prev = nullptr;
        rotation = rot;
        time_slot = ts;
    }

public:
    int rotation;                       /* 记录定时器在时间轮转多少圈后生效 */
    int time_slot;                      /* 记录定时器属于时间轮上的哪个槽(对应的链表,下同) */
    void (*cb_func)(client_data *);     /* 定时器回调函数 */
    client_data *user_data;             /* 客户端数据 */
    tw_timer *next;                     /* 指向下一个定时器 */
    tw_timer *prev;                     /* 指向前一个定时器 */
};


#endif //TIMER_WHEEL_TW_TIMER_H
  • tw_timer.cpp
//
// Created by yongpu on 2019/12/15.
//

#include "tw_timer.h"

  • time_wheel.h
//
// Created by yongpu on 2019/12/15.
//

#ifndef TIMER_WHEEL_TIME_WHEEL_H
#define TIMER_WHEEL_TIME_WHEEL_H

#include 
#include 
#include 
#include "tw_timer.h"

class time_wheel {
public:
    time_wheel();

    ~time_wheel();

    /* 根据定时值timeout创建一个定时器,并把它插入到合适的槽中 */
    tw_timer *add_timer(int timeout);

    /* 删除目标定时器timer */
    void del_timer(tw_timer *timer);

    /* SI时间到后,调用该函数,时间轮向前滚动一个槽的间隔 */
    void tick();

private:
    static const int N = 60;    /* 时间轮上槽的数量 */
    static const int SI = 1;    /* 每1 s时间轮转动一次,即槽间隔为1 s */
    tw_timer* slots[N];         /* 时间轮的槽,其中每个元素指向一个定时器链表,链表无序 */
    int cur_slot;               /* 时间轮的当前槽 */
};


#endif //TIMER_WHEEL_TIME_WHEEL_H
  • time_wheel.cpp
//
// Created by yongpu on 2019/12/15.
//

#include "time_wheel.h"

time_wheel::time_wheel() {
    cur_slot = 0;
    for (int i = 0; i < N; i++) {
        slots[i] = nullptr;
    }
}

time_wheel::~time_wheel() {
    /* 遍历每个槽,并销毁其中的定时器 */
    for (int i = 0; i < N; i++) {
        /* 释放链表上的每个节点 */
        tw_timer *tmp = slots[i];
        while (tmp) {
            slots[i] = tmp->next;
            delete tmp;
            tmp = slots[i];
        }
    }
}

tw_timer *time_wheel::add_timer(int timeout) {
    if (timeout < 0) {
        return nullptr;
    }
    int ticks = 0;
    /* 下面根据带插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发,并将该滴答
     * 数存储于变量ticks中。如果待插入定时器的超时值小于时间轮的槽间隔SI,则将ticks
     * 向上折合为1,否则就将ticks向下折合为timeout/SI */
    if (timeout < SI) {
        ticks = 1;
    } else {
        ticks = timeout / SI;
    }
    /* 计算待插入的定时器在时间轮转动多少圈后被触发 */
    int rotation = ticks / N;
    /* 计算待插入的定时器应该被插入到哪个槽中 */
    int ts = (cur_slot + (ticks % N)) % N;
    /* 创建新的定时器,它在时间轮转动ratation圈之后被触发,且位于第ts个槽上 */
    tw_timer *timer = new tw_timer(rotation, ts);
    /* 如果第ts个槽中无任何定时器,则把新建的定时器插入其中,并将该定时器设置为该槽的头结点 */
    if (slots[ts] == nullptr) {
        printf("add timer, rotation is %d,cur_slot is %d\n", rotation, ts, cur_slot);
        slots[ts] = timer;
    } else {
        /* 头插法在链表中插入节点 */
        timer->next = slots[ts];
        slots[ts]->prev = timer;

        slots[ts] = timer;
    }
    return timer;
}

void time_wheel::del_timer(tw_timer *timer) {
    if (timer == nullptr) {
        return;
    }
    int ts = timer->time_slot;
    /* slots[ts]是目标定时器所在槽的头结点。如果目标定时器就是该头结点,则需要
     * 重置第ts个槽的头结点 */
    if (timer == slots[ts]) {
        slots[ts] = slots[ts]->next;
        if (slots[ts]) {
            slots[ts]->prev = nullptr;
        }
        delete timer;
    } else {
        timer->prev->next = timer->next;
        if (timer->next) {
            timer->next->prev = timer->prev;
        }
        delete timer;
    }
}

void time_wheel::tick() {
    tw_timer *tmp = slots[cur_slot];    /* 取得时间轮上当前槽的头结点 */
    printf("current slots is %d\n", cur_slot);
    while (tmp) {
        printf("tick the timer once\n");
        /* 如果定时器的ratation值大于0,则它在这一轮中不起作用 */
        if (tmp->rotation > 0) {
            tmp->rotation--;
            tmp = tmp->next;
        }
            /* 否则说明定时器已经到期,于是执行定时任务,然后删除该定时器 */
        else {
            tmp->cb_func(tmp->user_data);
            if (tmp == slots[cur_slot]) {
                printf("delete header in cur_slot\n");
                slots[cur_slot] = tmp->next;
                delete tmp;
                if (slots[cur_slot]) {
                    slots[cur_slot]->prev = nullptr;
                }
                tmp = slots[cur_slot];
            } else {
                tmp->prev->next = tmp->next;
                if (tmp->next) {
                    tmp->next->prev = tmp->prev;
                }
                tw_timer *tmp2 = tmp->next;
                delete tmp;
                tmp = tmp2;
            }
        }
    }
    /* 更新时间轮的当前槽,以反映时间轮的转动 */
    cur_slot = cur_slot + 1;
    cur_slot = cur_slot % N;
}

参考博客

https://blog.csdn.net/weixin_33860528/article/details/94602216

你可能感兴趣的:(网络编程学习)