仿muduo库项目铺垫1!
感谢佬们支持!
目录
前言
在开发仿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哦。
