TimeWheel时间轮算法原理及实现(附源码)

前言

 在各种业务场景中,我们总是会需要一些定时进行一些操作,这些操作可能是需要在指定的某个时间点操作,也可能是每过一个固定的时间间隔后进行操作,这就要求我们需要有一个定时调度的逻辑,同时,这种定时操作,既有可能在某一刻数量比较密集,也有可能时间间隔比较密集,这就要考虑定时调度器对性能的影响.

如果在业务逻辑中,存在数量较大的定时任务,且每个定时任务都创建一个只属于自己的的调度管理器负责自身的生命周期及周期任务执行, 这就极大的浪费cpu的资源,降低自身性能.时间轮算法是一种调度模型,可以有效地利线程资源来处理批量周期任务,时间轮调度模型将数量巨大的定时任务绑定在单个调度器上,并统一使用这个调度器来管理,触发以及执行任务.这种模型使得大量延时任务,周期任务以及通知任务的管理变得高效.

1.时间轮核心

 时间轮算法的核心是,轮询线程不再是负责遍历所有任务,而只在负责处于其当前时间上的任务.就像钟表一样,时间轮有一个指针会一直旋转,每转到一个新的时间,就去执行挂在这一时刻下的所有任务,当然,这些任务通常是交给其他线程去做,指针要做的只是继续往下转,不会关心任务的执行进度.

 下面,我们就从一个最简单的定时任务来一步步优化,看看时间轮到底是怎么设计出来的

2.简单定时器

 这种方式最简单,如果想定期执行一个操作,只需要起一个定时器,设置时间间隔,设置回调函数,让它跑就完了.在定时任务非常少的情况下,这种方式没什么问题.如果定时任务的数目很大,并且都有不同的周期,那就产生了非常多的定时器, 这对系统的内存cpu都产生了很大的压力,程序还没开始跑呢,定时器已经满天飞了…

3.任务队列

 为了不产生过多的定时器,我们只使用一个定时器,将所有的定时任务放到一个队列中,每个定时任务都保存一份自己的定时信息,定时器每隔一个周期轮询一遍队列中的所有任务,如果任务的超时时间已到,则执行该任务,如果超时时间还没到,则将该任务的定时信息减掉一个定时器的时间间隔,等到完全减为0时,该任务就可以被执行了, 这个定时器一直这么执行并轮询下去.假设当前定时任务总数有100个,那定时器每个周期会遍历一个100个元素的队列,听上去还可以,那要有1000个的时候,10000个时候,这定时器就太可怜了,像一头老牛😄

4.优化任务队列

 为了解决任务队列中任务太多,一个定时器压力太大的问题,我们继续对其进行优化,既然所有的定时任务都放在一个队列下不太行,那就对定时任务进行分类,将定时周期相同的任务放在同一个定时器下,这样每个定时器的压力就会大大减小,每个定时器只负责自己周期内的任务,不负责其他周期的任务.但是如果每个任务的周期都相同,还是要产生很多定时器,似乎还是无法从根本上解决问题…

5.简单时间轮

 这时候时间轮的优势就体现出来了,我们设置一个环状的时间队列,队列上的每一个元素,我们称为一个槽(slot),这个槽所对应的时间在整个时间轮里是唯一的, 根据前面的分析也能知道,槽里面是放任务的,而且肯定不会放一个任务,而是放一个任务队列.

 对这个环状队列,我们维护一个指针,指针指向的槽,就是已经到达超时时间的槽,槽里的任务就要被执行.任务在被插入时间轮的时候,就根据当前时间以及自己的时间周期,确定好了自己会处于时间轮中的哪个槽.等到时间轮指针指到这个槽,任务就被触发.

 这样,时间轮只需要定时的轮询轮上的槽,如果有任务就交给任务处理线程去做,没有就继续轮询,即使总任务有一万个,调度器还是只会轮询这十个槽,而不会去轮询一遍十万个任务.

 这就是单层时间轮,这个时间轮的所能处理的最大周期是有限的,比如,一个具有10个slot的时间轮,wheel size = 10,每两个槽之间的间隔为1s,这个间隔称为tick,即最小的时间间隔,那么这个时间轮的跨度就是10*1 = 10s,也就是所支持能设置的最大周期为10s,如果一个任务每隔11秒执行一次.

 同时,10s这个周期太短了,现在各种系统中不乏周期为成百上千秒的定时任务,且以1s为分割,颗粒太大了,秒级以下的定时任务无法被触发.

 如果仅仅是个时间跨度为10s切以秒为tick的时间轮,是基本满足不了大部分场景的,为了满足需求,最简单快速的方法就是加大时间轮跨度来提升周期,降低tick来提高精度,如果时间跨度提升为60s, tick改成10ms,就需要6000个槽来安插任务,这样就可以设置周期更长的任务,可以根据更精细的时间单位(10ms)来执行定时任务

 需求满足了,但同时又带来了一些问题.

  • 轮询线程遍历效率低下问题:当timescale数量增加,task数量较少时,轮询线程遍历效率下降,比如只有50槽上有task,但是却需要遍历6000个timescale。这违背了我们时间轮算法的初衷:解决遍历轮询线程遍历效率低下的问题
  • 内存空间浪费问题:时间尺度密集,任务数量少,大部分时间尺度占用的内存空间没有意义。

 如果将时间轮跨度设置为1小时,那么整个时间轮需要60x60x1000/100 = 36000个单位的时间刻度,此时时间轮算法的遍历线程会遇到更大的运行效率低下。

 因此简单(单层)时间轮的性能上限很低,一旦精度和时间跨度要求上来,就无法达到期望的目标了.

6.多层时间轮

 在上面的场景下,多层时间轮就诞生了,就像我们生活中见过的水表一样,有非常多的小表盘

 多层时间轮的概念也非常清晰,将时间轮分为多个,每两个轮之间是进位的关系,例如最普遍的秒,分,时.

即:

  • 秒级时间轮上,设有60个槽, 每两个槽之间的时间为1s.

  • 分钟级时间轮上,设有60s个槽,每两个槽之间的时间为1min

  • 小时级时间轮上,设有24个槽,每两个槽之间的时间为1h.

 这样,秒级每走60个槽,时间过去一分钟,秒级时间轮归零,分级时间轮走一个槽; 分级时间轮每走过60个槽,时间过去一小时,分级时间轮归零,小时级时间轮走一个槽.

 通过三级时间轮,只需要遍历60+60+60 =180个槽,就可以成为一个精度为1s, 周期为60x60x60 = 216000s的定时调度器.

 多级时间轮的思想在很多开发中间件中都被应用,例如NettyAkkaQuartzZooKeeperKafka等等.

 作为学习Linux上C++开发的必备书籍,《Linux高性能服务器编程》以及《Linux多线程服务端编程:使用muduo C++网络库》两本书中,都介绍到了时间轮定时器,其中第二本书中,作者陈硕详细介绍了如果将时间轮应用在经典项目:muduo C++网络库中,非常值得参考学习!

 参照《Linux高性能服务器编程》的第11章:11.4高性能定时器,

 以及《Linux多线程服务端编程:使用muduo C++网络库》第7章:7.10 用timing wheel 踢掉空闲连接,

 简单实现了下一个三级定时器,下面是源码,并且在关键的地方进行了注释.

TimWheel.h

#include <memory>
#include <list>
#include <vector>
#include <mutex>

typedef struct TimePos{
    int pos_ms;
    int pos_sec;
    int pos_min;
}TimePos_t;

typedef struct Event {
    int id;
    void(*cb)(void);
    void* arg;
    TimePos_t timePos;
    int interval;
}Event_t;


class TimeWheel {
    typedef std::shared_ptr<TimeWheel> TimeWheelPtr;
    typedef void (*EventCallback_t)(void );
    typedef std::vector<std::list<Event_t>> EventSlotList_t;
public:
    TimeWheel();
    
    void initTimeWheel(int steps, int maxMin);
    void createTimingEvent(int interval, EventCallback_t callback);

public:
    static void* loopForInterval(void* arg);
    
private:
    int getCurrentMs(TimePos_t timePos);
    int createEventId();
    int processEvent(std::list<Event_t> &eventList);
    void getTriggerTimeFromInterval(int interval, TimePos_t &timePos);
    void insertEventToSlot(int interval, Event_t& event);

    EventSlotList_t m_eventSlotList;
    TimePos_t m_timePos;
    pthread_t m_loopThread;

    int m_firstLevelCount;
    int m_secondLevelCount;
    int m_thirdLevelCount; 
    
    int m_steps;
    int m_increaseId;  // not used
    std::mutex m_mutex;
};

TimeWheel.cpp

#include "TimeWheel.h"

#include <iostream>
#include <memory.h>
#include <chrono>
#include <thread>

TimeWheel::TimeWheel() : m_steps(0), m_firstLevelCount(0), m_secondLevelCount(60), m_thirdLevelCount(0),
                         m_increaseId (0){
                            memset(&m_timePos, 0, sizeof(m_timePos));
                         }

void* TimeWheel::loopForInterval(void* arg)
{
    if(arg == NULL) {
        printf("valid parameter\n");
        return NULL;
    }
    TimeWheel* timeWheel = reinterpret_cast<TimeWheel*>(arg);
    while(1) {
       std::this_thread::sleep_for(std::chrono::milliseconds(timeWheel->m_steps));
        // printf("wake up\n");
        TimePos pos = {0};
        TimePos m_lastTimePos = timeWheel->m_timePos;
        //update slot of current TimeWheel
        timeWheel->getTriggerTimeFromInterval(timeWheel->m_steps, pos);
        timeWheel->m_timePos = pos;
        {
            std::unique_lock<std::mutex> lock(timeWheel->m_mutex);
            // if minute changed, process in integral point (minute)
            if (pos.pos_min != m_lastTimePos.pos_min)
            {
                // printf("minutes changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_min + timeWheel->m_firstLevelCount + timeWheel->m_secondLevelCount];
                timeWheel->processEvent(*eventList);
                eventList->clear();
           }
            else if (pos.pos_sec != m_lastTimePos.pos_sec)
            {
                //in same minute, but second changed, now is in this integral second
                // printf("second changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_sec + timeWheel->m_firstLevelCount];
                timeWheel->processEvent(*eventList);
                eventList->clear();
            }
            else if (pos.pos_ms != m_lastTimePos.pos_ms)
            {
                //now in this ms
                // printf("ms changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_ms];
                timeWheel->processEvent(*eventList);
                eventList->clear();
            }
            // printf("loop over\n");
        }
     
    }

    return nullptr;
}

//init TimeWheel's step and maxmin, which detemine the max period of this wheel
void TimeWheel::initTimeWheel(int steps, int maxMin)
{
    if (1000 % steps != 0){
        printf("invalid steps\n");
        return;
    }
    m_steps = steps;
    m_firstLevelCount = 1000/steps;
    m_thirdLevelCount = maxMin;

    m_eventSlotList.resize(m_firstLevelCount + m_secondLevelCount + m_thirdLevelCount);
    int ret = pthread_create(&m_loopThread, NULL, loopForInterval, this);
    if(ret != 0) {
        printf("create thread error:%s\n", strerror(errno));
        return;
    }
    // pthread_join(m_loopThread, NULL);
}

void TimeWheel::createTimingEvent(int interval, EventCallback_t callback){
    if(interval < m_steps || interval % m_steps != 0 || interval >= m_steps*m_firstLevelCount*m_secondLevelCount*m_thirdLevelCount){
        printf("invalid interval\n");
        return;
    }
    printf("start create event\n");
    Event_t event = {0};
    event.interval = interval;
    event.cb = callback;
    //set time start
    event.timePos.pos_min = m_timePos.pos_min;
    event.timePos.pos_sec = m_timePos.pos_sec;
    event.timePos.pos_ms = m_timePos.pos_ms;
    event.id = createEventId();
    // insert it to a slot of TimeWheel
     std::unique_lock<std::mutex> lock(m_mutex);
    insertEventToSlot(interval, event);
    printf("create over\n");
}


int TimeWheel::createEventId() {
    return m_increaseId++;  
}


void TimeWheel::getTriggerTimeFromInterval(int interval, TimePos_t &timePos) {
    //get current time: ms
    int curTime = getCurrentMs(m_timePos);
    // printf("interval = %d,current ms = %d\n", interval, curTime);

    //caculate which slot this interval should belong to 
    int futureTime = curTime + interval;
    // printf("future ms = %d\n", futureTime);
    timePos.pos_min =  (futureTime/1000/60)%m_thirdLevelCount;
    timePos.pos_sec =  (futureTime%(1000*60))/1000;
    timePos.pos_ms = (futureTime%1000)/m_steps;

    // printf("next minPos=%d, secPos=%d, msPos=%d\n", timePos.pos_min, timePos.pos_sec, timePos.pos_ms);
    return;
}

int TimeWheel::getCurrentMs(TimePos_t timePos) {
    return m_steps * timePos.pos_ms + timePos.pos_sec*1000 +  timePos.pos_min*60*1000;
}

int TimeWheel::processEvent(std::list<Event_t> &eventList){
    // printf("eventList.size=%d\n", eventList.size());

    //process the event for current slot
    for(auto event = eventList.begin(); event != eventList.end(); event ++) {
        //caculate the current ms
        int currentMs = getCurrentMs(m_timePos);
        //caculate last  time(ms) this event was processed
        int lastProcessedMs = getCurrentMs(event->timePos);
        //caculate the distance between now and last time(ms)
        int distanceMs = (currentMs - lastProcessedMs + (m_secondLevelCount+1)*60*1000)%((m_secondLevelCount+1)*60*1000);

        //if interval == distanceMs, need process this event
        if (event->interval == distanceMs)
        {
            //process event
            event->cb();
            //get now pos as this event's start point
            event->timePos = m_timePos;
            //add this event to slot
            insertEventToSlot(event->interval, *event);
        }
        else
        {
            //this condition will be trigger when process the integral point 
            printf("event->interval != distanceMs\n");
            // although this event in this positon, but it not arriving timing, it will continue move to next slot caculate by distance ms.
            insertEventToSlot(distanceMs, *event);
        }
    }
    return 0;
}

void TimeWheel::insertEventToSlot(int interval, Event_t& event)
{
    printf("insertEventToSlot\n");

    TimePos_t timePos = {0};

    //caculate the which slot this event should be set to
    getTriggerTimeFromInterval(interval, timePos);
     {
        // printf("timePos.pos_min=%d, m_timePos.pos_min=%d\n", timePos.pos_min, m_timePos.pos_min);
        // printf("timePos.pos_sec=%d, m_timePos.pos_sec=%d\n", timePos.pos_sec, m_timePos.pos_sec);
        // printf("timePos.pos_ms=%d, m_timePos.pos_ms=%d\n", timePos.pos_ms, m_timePos.pos_ms);

        // if minutes not equal to current minute, first insert it to it's minute slot
        if (timePos.pos_min != m_timePos.pos_min)
        {
            printf("insert to %d minute\n", m_firstLevelCount + m_secondLevelCount + timePos.pos_min);
                m_eventSlotList[m_firstLevelCount + m_secondLevelCount + timePos.pos_min]
                    .push_back(event);
        }
        // if minutes is equal, but second changed, insert slot to this  integral point second
        else if (timePos.pos_sec != m_timePos.pos_sec)
        {
            printf("insert to %d sec\n",m_firstLevelCount + timePos.pos_sec);
            m_eventSlotList[m_firstLevelCount + timePos.pos_sec].push_back(event);
        }
        //if minute and second is equal, mean this event will not be trigger in integral point, set it to ms slot
        else if (timePos.pos_ms != m_timePos.pos_ms)
        {
            printf("insert to %d ms\n", timePos.pos_ms);
            m_eventSlotList[timePos.pos_ms].push_back(event);
        }
     }
    return;
}

main.cpp

#include <iostream>
#include "TimeWheel.h"
using namespace std;

void funccc(void) {
    cout << "exec function" << endl;
}

int main()
{
    TimeWheel wheel;
    wheel.initTimeWheel(100, 10);
    wheel.createTimingEvent(200, funccc);
    while (1)
    {
       
    }
}

 对于上述实现的三级时间轮,下篇文章将会详细拆解其各个步骤,然后大家就可以自己撸一个时间轮了!

 对于学习Linux开发以及C/C++开发,除了上文提到的书之外,《C++ Primer》以及《Efficetive C++》等数也是必不可少.这些书籍网上资源也有很多,但是一本一本收集起来还是挺费劲的,如果需要这些书的话,我将其整理到了公众号 上,公众号搜索 程序员DeRozan,回复1207即可直接拿到整理好的学习资料.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Michael.Scofield

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值