定时器/时间轮

仿muduo库项目铺垫1!
感谢佬们支持!


目录

系列文章目录

  • 前言
  • 一、Linux定时器接口介绍
  • timer_create
  • timer_settime
  • timer_gettime
  • 代码演示
  • 二、时间轮timewheel的实现
  • 思想
  • TimerTask类
  • 总结

前言

在开发仿muduo库这样的服务器项目时,要考虑到超时连接释放的逻辑,而所用到的就是时间轮的思想


一、Linux定时器接口介绍

首先在linux提供的计时器一共有三个接口:timer_create,timer_settime,timerfd_gettime

timer_create

cpp 复制代码
#include <sys/timerfd.h>

       int timerfd_create(int clockid, int flags);

      

首先得益于linux的学习基础,我们在看到这个接口时也能猜到,这个定时器他一定是文件!

所以timer_create的返回值一定是文件描述符fd

再看参数

clockid 最常用的就是这两个

real time:顾名思义就是系统开机时间,所以如果我们修改了系统时间,那么定时器的行为当然就很危险,故我们一般不采用
moment time 是从开机到现在的相对时间 较为稳妥


flags我们只关注阻塞/非阻塞即可

假设超时时间是3s,所谓阻塞呢就是你在创立timerfd之后,他超时了也不返回,每次超时之后就会向fd写入一个8字节数据1

所以假设我们创立之后,30s之后读取,我们就会得到一个10,表示从超时到现在超时10次


timer_settime

cpp 复制代码
int timerfd_settime(int fd, int flags,
                           const struct itimerspec *new_value,
                           struct itimerspec *_Nullable old_value);

set函数 fd不必多说,因为timerfd也是fd

flags也不必多说:0-相对时间 1-绝对时间,通常设为0即可

下来又是linux的经典了,是个新结构体 itimerspec

cpp 复制代码
struct itimerspec {
           struct timespec  it_interval;  /* Interval for periodic timer */
           struct timespec  it_value;     /* Initial expiration */
       };

这个结构体有的意思,一个表示第一次之后的超时间隔时间,一个表示第一次超时间

我有一个很好的例子

众所周知,pvz的豌豆射手有属性,假设你在有僵尸的一路上种下豌豆射手

那么第一次种下后他会在0.01-1.50s的时间间隔后发射第一个豌豆

此后会已1.36-1.50s的频率发射豌豆

此时0.01-1.50s就是it_calue,1.36-1.50s就是it_interval

搞定了结构体,再看这两个参数,一个是new,一个是old

那很显而易见了

new是用于设定定时器的新超时时间

old一定是用于恢复还原的旧时间,没什么用,给个空就好


timer_gettime

cpp 复制代码
int timerfd_gettime(int fd, struct itimerspec *curr_value);

一个fd,加上一个输出型参数。没什么好说的咱们也用不到


代码演示

写一波代码!我们设置一个3s超时时间间隔的定时器,并且设置第一次超时的时间也是3s

我们每次死循环读取timerfd的内容,并记录经过时间

复制代码
#include<time.h>
#include<sys/timerfd.h>
#include<unistd.h>
#include<stdint.h>

#include<iostream>

using namespace std;

int main()
{
    //搞一个定时器
    int timerfd=timerfd_create(CLOCK_MONOTONIC,0);

    struct itimerspec itm;
    itm.it_value.tv_sec=3;
    itm.it_value.tv_nsec=0;
    itm.it_interval.tv_sec=3;
    itm.it_interval.tv_nsec=0;

    timerfd_settime(timerfd,0,&itm,NULL);


    time_t start=time(NULL);

    while(1)
    {
        
        uint64_t tmp=0;
        int ret=read(timerfd,&tmp,sizeof(tmp));
        if(ret<0)
        {
            return -1;
        }
        cout<<"到点啦!超时次数:"<<tmp<<"经过时间:"<<time(NULL)-start<<endl;
        //sleep(1);

    }
    close(timerfd);

    return 0;
}

得到的效果就是每3秒超时一次


二、时间轮timewheel的实现

思想

有了上面的基础,我们来考虑时间轮。

成千上万个定时任务建立后,我们怎么知道哪个到时间哪个没到呢?如果遍历的话那就太慢了

而且对于大量的链接,考虑释放非活跃链接的话。如果这个链接在超时前又触发了io事件

那我们该怎么重新设置io事件呢?


我们可以引入一种时间轮的思想:假设我们定了一个3点的闹钟,那么当时针走到3点时候,

那么就表示到点了

同样道理,给一个数组和一个指针tick,让tick从0开始走,每秒移动一次,走到那个位置哪个位置的任务就要执行。(当然走到数组末尾还是要通过取余翻回来)

当然这个东西还是有缺陷的,假设我们想定一个60s的闹钟,那要等tick走60下吗?或者是5min的闹钟,那就是300下,这太麻烦了

所以我们可以采取多层级的时间轮:秒针轮,分针轮,时针轮;由此一级一级,可以设置超长时间的时间轮啦。

图中设置秒级时间轮为60,分级为60,时级为24,最大就可以有1天的最大超时时间。


考虑好模型后,我们还有一个要考虑的问题

1:一个时刻只能放一个任务怎么办呢?

这个好办,我们可以像哈希表一样,在每个位置上挂一个任务链表!但是这里我们采用二维数组

2: 下来要解决的是如果一个连接被刷新了io事件,那么如何让这个连接不被释放?延时他的时间。

作为一个时间轮定时器,本身是不关注任务类型的,到了点就执行,所以我们可以设定一个

TimerTask类,再用shared_ptr维护他,所以释放时调用析构就可执行

也就是说

·用TimerTask类封装任务后,类实例化出每个对象,就是每个任务,当对象销毁时,就会去调用析构函数,把执行任务的函数放到析构里,也就执行了任务

·接下来再看刷新连接的逻辑,众所周知,shared_ptr是有一个引用计数的,当引用计数变成0时,指针指向的对象才会销毁,所以刷新连接的逻辑就是想办法在一个新的时间节点创立一个对这个任务对象的shared_ptr,让引用计数++,这样即使原来的shared_ptr到了时间销毁了,任务对象也不会销毁

但是这里又有个问题了,就是如何保证新创建的shared_ptr一定和初始的shared_ptr共用同一个控制块呢也就是共享同一引用计数

cpp 复制代码
int *a=new int();

    shared_ptr<int> p1(a);
    shared_ptr<int> p2(p1);
    shared_ptr<int> p3(a);

在这个例子中,a是我们的原始对象

p1首先是初始shared_ptr,p2是对p1的拷贝,所以可以共享空间

但是p3却对任务对象a又创立了shared_ptr,p3和p1互相不知道彼此啊

最后导致a被销毁两次

最终得到一个什么结论呢

我们一定要能找到初始的shared_ptr,再拷贝构造一个。

这个时候的处理办法就很牛逼了

我们用初始shared_ptr创建一个不增加引用计数的weak_ptr

我们建立一个任务id:weakptr的哈希表,每次想刷新的时候,通过任务id找到老shared_ptr,如果

老shared_ptr还活着,我们就拷贝一个新shared_ptr加进去,如果死了,那说明连接已经没了

就不用刷新。


开始编码!

TimerTask

首先是一个封装任务的timertask类

成员来一个任务id,一个超时时间timeout,一个执行的回调函数(也就是执行任务的函数)

还有传递一个销毁任务的回调函数(其实就是在哈希表中删除映射)

还有一个成员是_cancel,当_cancel设置为true,表示这个任务被销毁。

为什么要这么设计呢?

如果直接调用任务的析构函数会导致任务被执行!所以设置一个标识位,当设置标志位

_cancel为true时,这个任务就不执行了


接口:一个构造,一个析构,一个返回id,一个返回延迟时间,一个设置删除回调,一个设置

取消标志位。

cpp 复制代码
class TimerTask
{
public:
    TimerTask(u_int64_t id,int delay,const TaskFunc& cb)
    :_id(id),_timeout(delay),_task_cb(cb),_canceled(false)
    {}

    //析构要执行任务
    ~TimerTask()
    {
     
    }

    uint Id()
    {

    }

    int DelayTime()
    {
        
    }

    //设置删除映射的回调
    void SetRelease(const ReleaseFunc& cb)
    {
       
    }

    void Cancel()
    {
        
    }

private:
    uint64_t _id;//任务id
    int _timeout;//超市时间
    bool _canceled=false;
    TaskFunc _task_cb;//执行任务的回调
    ReleaseFunc _release;//销毁任务的回调

};

然后接口的实现还是简单的

cpp 复制代码
class TimerTask
{
public:
    TimerTask(u_int64_t id,int delay,const TaskFunc& cb)
    :_id(id),_timeout(delay),_task_cb(cb),_canceled(false)
    {}

    //析构要执行任务
    ~TimerTask()
    {
        if(_canceled==false)
        {
            _task_cb();//执行回调
        }
        _release();//删映射关系的函数
    }

    uint Id()
    {
        return _id;
    }

    int DelayTime()
    {
        return _timeout;
    }

    //设置删除映射的回调
    void SetRelease(const ReleaseFunc& cb)
    {
        _release=cb;
    }

    void Cancel()
    {
        _canceled=true;
    }

private:
    uint64_t _id;//任务id
    int _timeout;//超市时间
    bool _canceled=false;
    TaskFunc _task_cb;//执行任务的回调
    ReleaseFunc _release;//销毁任务的回调

};

TimerWheel

下来是TimerWheel类的实现:

首先成员函数:一个二维数组的wheel,一个id:weakptr的哈希表,一个表示指针的指针_tick

还有用来记录最大超时时间的容量_capacity

cpp 复制代码
using WeakTask=weak_ptr<TimerTask>;
using PtrTask=shared_ptr<TimerTask>;

class TimerWheel
{
public:
    TimerWheel()
    :_tick(0),_capacity(60),_wheel(_capacity)
    {}

    ~TimerWheel()
    {}

    //添加定时任务
    void TimerAdd(uint64_t id,int delay,const TaskFunc& cb)
    {
        
    }   

    void RefreshTask(uint64_t id)
    {
        
    }

    //应该每隔1s执行一次!
    void RunTimerTask()
    {
        
    }

    void TimerCancel(uint64_t id)
    {
        
    }

private:
    int _tick=0;
    int _capacity=60;

    vector<vector<PtrTask>> _wheel;
    unordered_map<uint64_t,WeakTask> _timers;
private:

    void RemoveTimer(uint64_t id)
    {
        
    }
};

我们先来看类内的RemoveTimer:其实就是在哈希表删掉映射

cpp 复制代码
void RemoveTimer(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            _timers.erase(it);
        }
    }

我们先来看添加任务的函数

有四个逻辑:1 创建shared_ptr 2 设置销毁回调(也就是咱们刚刚实现的RemoveTimer) 3 哈希表添加映射 4 添加到时间轮中

cpp 复制代码
//添加定时任务
    void TimerAdd(uint64_t id,int delay,const TaskFunc& cb)
    {
        PtrTask pt(new TimerTask(id,delay,cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer,this,id));
        _timers[id]=WeakTask(pt);
        _wheel[(_tick+delay)%_capacity].push_back(pt);
    }   

下来是刷新任务,就是我们之前说的 如果没有找到任务就不做事,有找到任务就

通过哈希表找到weakptr,再找到shared_ptr构造一个新ptr,再加入timerwheel

cpp 复制代码
void RefreshTask(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            PtrTask pt=it->second.lock();
            int delay=pt->DelayTime();

            _wheel[(_tick+delay)%_capacity].push_back(pt);
        }
    }

最后的RunTimer和CancelTimer 一个是每秒运行一下;一个是调用Cancel

cpp 复制代码
    void RunTimerTask()
    {
        _tick=(_tick+1)%_capacity;
        _wheel[_tick].clear();//调析构函数就可以执行回调啦!
    }

    void TimerCancel(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            //_timers.erase(it);
            PtrTask pt=it->second.lock();
            if(pt)
            {
                pt->Cancel();
            }
        }
    }

结束!

我们来简单测试一下


测试

写一个测试类和main函数

cpp 复制代码
//测试类
class Test
{
public:
    Test()
    {
        std::cout<<"构造"<<std::endl;
    }

    ~Test()
    {
        std::cout<<"析构"<<std::endl;
    }

};

void del(Test *t)
{
    delete t;
}

int main()
{
    
    TimerWheel tw;
    Test *t =new Test();

    tw.TimerAdd(888,5,std::bind(del,t));
    for(int i=0;i<5;++i)
    {
        tw.RefreshTask(888);
        tw.RunTimerTask();
        std::cout<<"刷新了一下任务"<<std::endl;
        sleep(1);
    }
    std::cout<<"死啦!曹操了"<<std::endl;


    return 0;
}


完整代码

cpp 复制代码
#include<iostream>
#include<functional>
#include<vector>
#include<memory>
#include<unordered_map>
#include<cstdio>

#include<unistd.h>
#include<sys/timerfd.h>
#include<fcntl.h>

using TaskFunc=std::function<void()>;
using ReleaseFunc=std::function<void()>;

using std::vector;
using std::unordered_map;
using std::weak_ptr;
using std::shared_ptr;


class TimerTask
{
public:
    TimerTask(u_int64_t id,int delay,const TaskFunc& cb)
    :_id(id),_timeout(delay),_task_cb(cb),_canceled(false)
    {}

    //析构要执行任务
    ~TimerTask()
    {
        if(_canceled==false)
        {
            _task_cb();//执行回调
        }
        _release();//删映射关系的函数
    }

    uint Id()
    {
        return _id;
    }

    int DelayTime()
    {
        return _timeout;
    }

    //设置删除映射的回调
    void SetRelease(const ReleaseFunc& cb)
    {
        _release=cb;
    }

    void Cancel()
    {
        _canceled=true;
    }

private:
    uint64_t _id;//任务id
    int _timeout;//超市时间
    bool _canceled=false;
    TaskFunc _task_cb;//执行任务的回调
    ReleaseFunc _release;//销毁任务的回调
};

using WeakTask=weak_ptr<TimerTask>;
using PtrTask=shared_ptr<TimerTask>;

class TimerWheel
{
public:
    TimerWheel()
    :_tick(0),_capacity(60),_wheel(_capacity)
    {}

    ~TimerWheel()
    {}

    //添加定时任务
    void TimerAdd(uint64_t id,int delay,const TaskFunc& cb)
    {
        PtrTask pt(new TimerTask(id,delay,cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer,this,id));
        _timers[id]=WeakTask(pt);
        _wheel[(_tick+delay)%_capacity].push_back(pt);
    }   

    void RefreshTask(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            PtrTask pt=it->second.lock();
            int delay=pt->DelayTime();

            _wheel[(_tick+delay)%_capacity].push_back(pt);
        }
    }

    //应该每隔1s执行一次!
    void RunTimerTask()
    {
        _tick=(_tick+1)%_capacity;
        _wheel[_tick].clear();//调析构函数就可以执行回调啦!
    }

    void TimerCancel(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            //_timers.erase(it);
            PtrTask pt=it->second.lock();
            if(pt)
            {
                pt->Cancel();
            }
        }
    }

private:
    int _tick=0;
    int _capacity=60;

    vector<vector<PtrTask>> _wheel;
    unordered_map<uint64_t,WeakTask> _timers;
private:

    void RemoveTimer(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())
        {
            return ;
        }
        else
        {
            _timers.erase(it);
        }
    }
};

//测试类
class Test
{
public:
    Test()
    {
        std::cout<<"构造"<<std::endl;
    }

    ~Test()
    {
        std::cout<<"析构"<<std::endl;
    }

};

void del(Test *t)
{
    delete t;
}

int main()
{
    #if 0
    int *a=new int();

    shared_ptr<int> p1(a);
    shared_ptr<int> p2(p1);
    shared_ptr<int> p3(a);

    #endif

    TimerWheel tw;
    Test *t =new Test();

    tw.TimerAdd(888,5,std::bind(del,t));
    for(int i=0;i<5;++i)
    {
        tw.RefreshTask(888);
        tw.RunTimerTask();
        std::cout<<"刷新了一下任务"<<std::endl;
        sleep(1);
    }
    std::cout<<"死啦!曹操了"<<std::endl;


    return 0;
}

总结

做总结,本篇博客介绍了定时器接口和写了时间轮的代码!

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

相关推荐
编程之升级打怪2 小时前
用排他锁来实现Python语言的变量值更新
开发语言·python
rrrjqy2 小时前
Java基础篇(二)
java·开发语言
我命由我123452 小时前
React - React 配置代理、搜索案例(Fetch + PubSub)、React 路由基本使用、NavLink
开发语言·前端·javascript·react.js·前端框架·html·ecmascript
沐知全栈开发2 小时前
R 循环:深度解析与高效运用
开发语言
C^h2 小时前
RTthread中的内存池理解
linux·数据库·c++·算法·嵌入式
程序员小寒2 小时前
JavaScript设计模式(四):发布-订阅模式实现与应用
开发语言·前端·javascript·设计模式
csbysj20202 小时前
JSON 语法
开发语言
郝学胜-神的一滴2 小时前
深入解析:生成器在UserList中的应用与Python可迭代对象实现原理
开发语言·python·程序人生·算法
为美好的生活献上中指2 小时前
*Java 沉淀重走长征路*之——《Linux 从入门到企业实战:一套六步法,带你打通运维与开发的任督二脉》
java·linux·运维·开发语言·阿里云·华为云·linux命令