一種基於最小堆和epoll的高性能定時器——時間堆

(1)時間堆的原理

    定時器以到期時間作爲排序值,存放於最小堆這種數據結構中。時間堆不以固定的頻率來查詢是否有定時器到期,而是每次當堆頂的定時器到期後才處理一次定時事件,避免了定期查詢導致的開銷。

 (2)難點分析及解決思路

Q:如何確定堆頂定時器是否到期?

A:仍然以定時的方式來提醒進程或線程有定時器到期,但定時時長是變化的,其值始終等於堆頂定時器到期的絕對時刻,與定時時刻之差;

Q:當插入一個新的定時器,或刪除一個定時器後,堆頂定時器可能會發生變化,因此定時時長也就可能變化,如何及時更新定時時長呢?

A:藉助epoll來監聽,插入或刪除定時器這一事件是否發生,一旦發生,則立即修改定時時長;

Q:如何實現定時呢?

A:可以使用alarm函數實現。但其實可以利用epoll_wait函數中的超時參數來進行定時。如果時間堆中無定時器,則超時參數爲-1,促使其阻塞,直到有定時器插入事件發生;否則,超時參數等於定時時長。當定時器插入或刪除事件發生後,epoll_wait函數返回,隨即重新確定定時時長,並修改超時參數,以用於下一次的epoll_wait函數調用。

(3)時間堆框架

 

(4)源代碼及註釋

#ifndef TIME_HEAP
#define TIME_HEAP

#include <time.h>
#include <vector>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <stdio.h>
#include <pthread.h>

template <typename T> //T是客戶類
class th_timer //定時器類
{
public:
    time_t expire; //定時器到期的絕對時刻
    T *user_data; //客戶數據,應包含此定時器指針
    void (*cb_func)(T*); //回調函數,即當定時器到期時,要執行的操作
    int loc; //定時器在時間堆中的位置,從堆中刪除定時器時會用到
public:
    th_timer(time_t _expire, T *_user, void (*_cb_func)(T*))
    :expire(_expire), user_data(_user), cb_func(_cb_func) { };
};

template <typename T> 
class time_heap //時間堆類
{
private:
    static const int INIT_SIZE = 50; //時間堆的初始容量
    static const int MAX_EVENT_NUMBER = 10; //epoll內核事件表所能容納的事件的最大數量
    std::vector<th_timer<T>*> min_heap; //用於表示時間堆的vector數組,從[1]開始使用
    int hsize; //堆的實際大小
    int epollfd; //指向epoll內核事件表的fd
    int pipefd[2]; //通信管道,當插入或刪除定時器事件發生時,用來傳遞信息
private: //私有工具函數
    void check_size()
    // 檢查時間堆是否已裝滿,若是則擴充容量
    {
        if(hsize == min_heap.size() - 1)
        {
            min_heap.resize(2 * min_heap.size());
        }
    }
    void swap(int loc_a, int loc_b)
    // 給定時間堆中的兩個位置,交換這兩處的元素,定時器記錄的自身位置也要及時更新
    {
        th_timer<T> *temp = min_heap[loc_a];

        min_heap[loc_a] = min_heap[loc_b];
        min_heap[loc_a]->loc = loc_a; //更新定時器自身位置

        min_heap[loc_b] = temp;
        min_heap[loc_b]->loc = loc_b;  //更新定時器自身位置
    }
    void move_down(int rt)
    // 從位置rt開始下移操作。當最小堆中的某個元素突然變大時,可用此函數維護最小堆的性質。
    {
        int lc = rt * 2, rc = lc + 1, min_loc = rt;
        if(lc <= hsize && min_heap[lc]->expire < min_heap[min_loc]->expire)
        {
            min_loc = lc;
        }
        if(rc <= hsize && min_heap[rc]->expire < min_heap[min_loc]->expire)
        {
            min_loc = rc;
        }
        if(min_loc != rt)
        {
            swap(min_loc, rt);
            move_down(min_loc); //遞歸下移
        }
    }
    void move_up(int rt)
    // 從位置rt開始上移操作。當最小堆中的某個元素突然變小時,可用此函數維護最小堆的性質。
    {
        th_timer<T>* timer = min_heap[rt];

        int pos = rt;
        for(;rt > 1 && timer->expire < min_heap[pos/2]->expire;pos /= 2)
        {
            min_heap[pos] = min_heap[pos / 2];
            min_heap[pos]->loc = pos;
        }

        min_heap[pos] = timer;
        min_heap[pos]->loc = pos;
    }
    static void* run(void *arg)
    //線程的執行函數,用於監聽插入或刪除定時器事件的發生,並及時修改定時時長,處理定時事件
    {
        time_heap* th = (time_heap*)arg; //獲取時間堆類對象

        //將通信管道[0]端插入epoll內核事件表內
        epoll_event event;
        event.data.fd = th->pipefd[0];
        event.events = EPOLLIN | EPOLLET;
        epoll_ctl(th->epollfd, EPOLL_CTL_ADD, th->pipefd[0], &event);

        epoll_event events[MAX_EVENT_NUMBER];

        bool stop = false;
        int timeout = -1; //超時參數,剛啓動時堆中無定時器,因此令epoll_wait阻塞,直到有定時器插入

        while(!stop)
        {
            int number = epoll_wait(th->epollfd, events, MAX_EVENT_NUMBER, timeout);

            if(number < 0 && errno != EAGAIN)
            {
                printf("timer epoll failure\n");
                break;
            }

            if(!number)
            //超時返回,說明堆頂定時器定時到期,故執行定時事件
            //並修改超時參數
            {
                timeout = th->tick();
            }
            else
            //正常返回,說明有插入/刪除定時器事件發生,故重新確定定時時長,並修改超時參數
            //此管道[0]端可省略讀數據操作,不影響[1]端往管道寫入新的數據
            //由於[0]端設爲ET模式,一旦有新的數據寫入,epoll會且僅會通知一次
            {
                //如果堆中還有定時器,則定時時長等於,堆頂定時器到期的絕對時刻與當前時刻之差
                //否則超時參數設爲-1,令epoll_wait阻塞
                timeout = th->hsize ? th->min_heap[1]->expire - time(NULL) : -1;
            }
            //秒到毫秒的轉換
            if(timeout != -1)
                timeout *= 1000;
        }
        pthread_exit(NULL);
    }
public:
    time_heap():hsize(0), epollfd(epoll_create(5))
    //默認構造函數
    {
        min_heap.resize(INIT_SIZE); //堆的容量初始化

        //將通信管道設爲全雙工模式
        int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
        assert(ret == 0);

        //創建線程,開始監聽插入或刪除定時器事件的發生
        pthread_t tid;
        ret = pthread_create(&tid, NULL, run, this);
        assert(ret == 0);
    }
    th_timer<T>* add_timer(int timeout, T *user, void (*cb_func)(T*))
    //用戶接口,往時間堆中插入一個新的定時器
    {
        if(timeout <= 0)
        {
            return NULL;
        }

        check_size(); //確保時間堆容量足夠

        //創建定時器
        th_timer<T>* timer = new th_timer<T>(timeout + time(NULL), user, cb_func); 

        //插到堆的末尾,並開始上移操作
        min_heap[++hsize] = timer;
        move_up(hsize);

        if(timer == min_heap[1])
        //新插入的定時器成爲新的堆頂,因此定時時長可能發生變化,故通知監聽線程
        {
            char msg = 1;
            send(pipefd[1], (char*)&msg, sizeof(msg), 0);
        }
        return timer;
    }
    void del_timer(th_timer<T> *timer)
    //用戶接口,給定定時器指針,從堆中刪除此定時器
    {
        if(!timer)
        {
            return;
        }

        int loc = timer->loc;
        swap(loc, hsize); //將此定時器與堆尾定時器交換位置
        hsize--; 
        move_down(loc); //此處元素可能變大,因此從此處開始下移操作

        delete timer;

        if(loc == 1) //被刪除的定時器是原堆頂,因此定時時長可能發生變化,故通知監聽線程
        {
            char msg = 1;
            send(pipefd[1], &msg, sizeof(msg), 0);         
        }
    }
    void set_timer(th_timer<T> *timer, int timeout)
    //用戶接口,修改一個定時器的到期時間
    {
        if(!timer || timeout <= 0)
        {
            return;
        }

        int old_expire = timer->expire; //記錄舊到期時間
        timer->expire = timeout + time(NULL); //修改到期時間

        if(old_expire < timer->expire)
        //到期時間增加了,則此處元素變大,故開始下移操作
        {
            move_down(timer->loc);
        }
        else if(old_expire > timer->expire)
        //到期時間減小了,則此處元素變小,故開始上移操作
        {
            move_up(timer->loc);
        }
        //簡化判斷,一律認爲定時時長可能發生變化,故通知監聽線程
        char msg = 1;
        send(pipefd[1], &msg, sizeof(msg), 0);
    }
    int tick()
    //堆頂定時器到期,執行定時事件,並返回新的定時時長
    {
        time_t cur_time = time(NULL); //記錄當前時間

        th_timer<T> *top = min_heap[1]; //獲得堆頂定時器
        do
        {
            top->cb_func(top->user_data); //執行定時事件
            del_timer(top); //刪除此定時器

            if(hsize > 0)
            //若堆中還有定時器,則繼續獲得堆頂定時器
            {
                top = min_heap[1];
            }
            else
            {
                break;
            }
        }while(top->expire <= cur_time); //不僅處理堆頂定時器,還處理所有已到期的定時器

        if(hsize == 0)
        //堆中無定時器,令epoll_wait阻塞
        {
            return -1;
        }
        else
        //確定新的定時時長,即堆頂定時器到期的絕對時刻與當前時刻之差
        {
            return top->expire - cur_time;
        }
    }
};

#endif

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章