C++实现简单的定时器

定时器概念:

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

在网络编程中,我们通过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()函数,通过与当前时间比较来判断是否为超时事件。如果是则处理类中的信号处理函数。

基于升序链表的定时器用起来很方便,但一旦定时事件多起来,效率就会很低。
因此,通过改进,就有了时间轮。
C++实现简单的定时器_第1张图片

指针每转动一下为一个滴答,图中指向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)。

C++实现简单的定时器_第2张图片C++实现简单的定时器_第3张图片C++实现简单的定时器_第4张图片

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

C++实现简单的定时器_第5张图片C++实现简单的定时器_第6张图片C++实现简单的定时器_第7张图片

下滤操作代码:(二叉树采用数组存储,对于任意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,一个时间轮完整周期完成。恩,有时间了回来实现吧。

你可能感兴趣的:(c++)