C++实现简单的定时器

来源:互联网 发布:网易数据分析师笔试题 编辑:程序博客网 时间:2024/05/16 05:20

定时器概念:

使用定时器的目的是周期性的执行一个任务,或者是到某一时间去执行某一任务。本章用来处理断开连接超时的客户端,为此,将每个定时时间封装成定时器,并使用链表,时间轮(也是链表),堆等容器类数据结构,对定时时间统一管理。

在网络编程中,我们通过socket创建套接字,然后通过setsockopt()函数设置套接口选项。

函数原型

setsockopt( SOCKET s, int level, int optname, const char FAR* optval, int option);

第三个参数optname可用来指定超时接受(SO_RCVTIMEO)或者超时发送(SO_SNDTIMEO),与其关联的第四个参数此时为timeout类型,指定具体的超时时间。然后用connect()函数去连接客户端,超时对应的errno是EINPROGRESS。检测到此errno则关闭连接。此处根据系统调用的返回值来判断超时时间是否已到,据此处理定时任务即关闭连接。

除了通过系统调用判断,更多的使用信号。SIGALRM是在定时器终止时发送给进程的信号。由alarm()和setitimer()函数设置的实时闹钟一旦超时,将触发此信号,然后在其处理函数中处理到期的任务。

为了便于处理多个同类型不同时间的定时事件,我们可以设计基于升序链表的定时器,即保持一个超时时间从小到大的时间有序的定时器链表。

主要部分有三个

1./*用户数据结构*/struct client_data{    sockaddr_in address;    /*客户端socket地址*/    int sockfd;   /*客户端套接字*/    char buffer[ BUFFER_SIZE ];    /*读缓冲区*/    util_timer* timer;    /*定时器*/};2./*定时器类*/struct util_timer{    public:        util_timer() : prev( NULL ), next( NULL ) {  }    public:        time_t expire;   /*任务的超时时间*/        void ( *cd_func )(client_data*);   /*任务回调函数*/        client_data* user_data;    /*用户数据结构*/        util_timer* prev;      /*双向链表*/        util_timer* next;};3./*定时器链表。它是一个升序、双向链表,且带有头结点和尾节点*/class sort_timer_lst{    public:        /*构造函数*/        sort_timer_lst() : head( NULL ), tail( NULL ) {}        /*析构函数,清空定时器链表*/        ~sort_timer_lst();        /*将目标定时器timer添加到链表中*/        void add_timer( util_timer* timer );        /*当某个定时任务发生变化时,调整对应的定时器在链表的位置,这个函数只考虑被调整的定时器事件延长情况,即只需向尾部移*/        void adjust_timer( util_timer* timer );        /*将目标定时器timer从链表中删除*/        void del_timer( util_timer* timer );        /*SIGALRM信号每次被触发就在其信号处理函数中执行一次tick函数,以处理链表上到期的任务*/        void tick();    private:        /*一个重载的辅助函数,被函数add和adjust调用,该函数表示将time添加到head之后的位置*/        void add_timer( util_timer* timer, util_timer* lst_head );    private:        util_timer* head;        util_timer* tail;};

信号处理函数

void tick(){    time_t cur = time(NULL);  /*获取当前时间*/    util_timer* temp = head;    while( temp )    {        if( cur < temp-> expire )  /*超过当前时间则为超时*/        {            break;        }        temp -> cb_func( tmp -> user_data );         /*...删除处理完的定时器,处理头结点...*/    }}

SIGALRM信号每次触发就执行一次tick()函数,通过与当前时间比较来判断是否为超时事件。如果是则处理类中的信号处理函数。

基于升序链表的定时器用起来很方便,但一旦定时事件多起来,效率就会很低。
因此,通过改进,就有了时间轮。
这里写图片描述

指针每转动一下为一个滴答,图中指向1的指针为当前槽,指向2的指针(本来是虚线来着,技术渣没画出来)指向下一个槽。
一个滴答的时间称为时间轮的槽间隔si(心博时间),时间轮共N个槽,因此运转一周时间为 N*si . 每个槽指向一个定时器链表,每个链表上的定时器具有相同的特征,他们的定时时间相差 N*si的整数倍。
如果N=12,指针指向了tick=2处,要加一个再经过tick为15的定时任务,怎么放置?
计算可得:2 + 15%12 = 5,所以将其放置在tick=5的槽中,待tick从2跳一轮回到2再跳3下到tick=5,这个事件就被执行。
简单的时间轮类(仅有一个轮子):

class tw_timer{public:    tw_timer(int rot, int ts);public:    int rotation; /*记录定时器再时间轮中转多少圈后生效*/    int time_slot;  /*记录定时器属于时间轮上哪个槽*/    void (*cb_func)( client_data* ); /*定时器回调函数*/    client_data* user_data;   /*客户数据*/    tw_timer* next;   /*指向下一个定时器*/    tw_timer* prev;   /*指向上一个定时器*/}class time_wheel{public:    time_wheel();    ~time_wheel();    /*根据定时值timeout创建一个定时器并插入合适槽中*/    tw_timerr* add_timer( int timeout );    /*删除目标定时器*/    void del_timer( tw_timer* timer );    /*SI时间到后,调用该哈数,时间轮像前滚动一个槽*/    void tick();}

添加或者删除一个定时器的复杂度是O(1),执行一个定时器的时间复杂度为O(n)。

时间堆

这章的学习中,感觉效率比较高的是时间堆。
因为处理定时器事件的时候都是超时时间最小的,所以可以采用最小堆的方式来打理这群定时器,即每个节点的值都小于或等于他的子节点的值的二叉树。

插入定时器

在树下创建一个空穴,将新节点插入,如果不影响堆序,则插入完成,否则执行上滤操作。即交换空穴和它的父节点上的元素,不断执行直到插入成功。例如插入值为14的元素。时间复杂度为O(lgn)。

时间堆的删除很简单,即删除堆的根结点即可。删除后调整堆序,先在根结点建立空穴,此时根结点已被删除。此时堆中少了一个元素,我们可以把堆最后一个元素X移动到该堆之外(但不能丢),如果X可以插入根据诶点,则删除成功。但一般不能,此时就对堆中元素执行上滤操作,即交换空穴和他的两个儿子中的较小者,不断进行此过程,直到X可以被成功插入,删除成功。删除一个定时器的时间复杂度为O(1)。

下滤操作代码:(二叉树采用数组存储,对于任意i位置,2i+1为左孩子)

time_heap::percolate_down( int hole )  //hole即空穴下标{    heap_timer* temp = array[ hole ];    int child = 0;    for( ; (hole*2+1) <= (cur_size-1); hole=child )    {        child = hole*2 + 1;        if( child < (cur_size-1) && (array[child+1] -> expire < array[child] -> expire))        {            ++child;                }        if( arrry[child] -> expire < temp -> expire )        {            array[hole] = array[child];        }        else        {            break;        }    }    array[hole]  = temp;}

定时器中的时间轮优化下,可以有多层轮子,提高精确度。比如初始化一个三层时间轮:秒刻盘:0~59个SecList, 分刻盘:0~59个MinList, 时刻盘:0~12个HourList。
SecTick由外界推动,每跳一轮(60格),SecTick复位至0,同时MinTick跳1格;
同理MinTick每跳一轮(60格),MinTick复位至0,同时HourTick跳1格;
最高层:HourTick跳一轮(12格),HourTick复位至0,一个时间轮完整周期完成。恩,有时间了回来实现吧。

1 0