C時間輪

看完了《linux高性能服務器編程》對裏面的定時器很感興趣。書中提到三種定時器,分別是:基於升序鏈表的定時器,基於時間輪的定時器,基於時間堆的定時器。三種定時器的實現書中均是給了C++代碼,不過我對C++不太感興趣,雖然現在在做C++開發,因此寫了C版本的。書中定時器只給了封裝的定時器類,沒有給調用層代碼,我是估摸着寫了調用層代碼。這裏做個總結,以後可以翻翻:

基於升序鏈表的定時器沒太大難度,因此也懶得總結了。

說一下時間輪,下面是截的書中的圖片

這裏寫圖片描述

時間輪,像輪子一樣滾動定時,每滾一個刻度,指針就走一個滴答,滾完一圈,就進入下一圈。因此有了這個概念,時間輪的結構也就出來了:1.齒輪(槽slot),用來標識一個滴答;2.槽間隔(slot interval ),當前槽經過多長時間到下一個槽;3.一圈的槽數量(N);4.當前指針,走一個滴答加一,走完一圈又回到初始位置。

再深入一點,定時器以什麼方式添加到槽上?可以看圖,每一個槽其實就是一個鏈表頭結點,定時器即添加到所屬槽的鏈表後。這樣我們可以對時間輪性能進行分析,SI越小,定時精度越高,如果SI=10s,那麼我們指定的定時器只能是10s的倍數;如果N越大,定時器效率越高,這也很好理解,N越小,一圈槽數量越少,那麼我們同樣添加100個定時器,分配到每個頭結點的定時器越多,每一次滴答到時,就遍歷當前槽,遍歷一次所花時間越多。

如何確定定時器位置?根據定時器到時時間可以計算,例如:定時器超時時間timeout=21s(即21s後觸發定時器),當前間隔SI=2s,一圈槽數量N=70,當前指針cur_slot指向第5個槽,我們可以計算出定時器放置的位置,這裏需要兩個變量,一個rotation指定定時器處於第幾圈,一個slot指定定時器處於第幾個槽,因此slot = ( cur_slot + timeout / SI ) % N = 15, rotation = timeout / SI / N = 0,即此定時器被放置於15槽的鏈表後,至於是鏈表頭插還是尾插這個隨意,指針滴答到了15槽即觸發15槽到時,遍歷15槽鏈表,若rotation=0的表示爲當前該觸發定時器,若rotation>0的定時器對rotation–(其實很好理解,cur_slot在轉當前輪,則不處理後面的輪,只對它的rotation減一就跳過,等到cur_slot轉下一圈再判斷此定時器)。根據這個計算,如果其它參數不變,現在有一個timeout=161s的定時器,cur_slot=5,我們可以計算出這個定時器的slot=15,rotation=1,正好處於第15槽,但是是下一轉觸發該觸發。

也就是說,如果我們根據以上參數,同時添加一個15s和一個161s定時器,他們都會隨時間輪輪轉觸發到,只不過指針第一次只想15槽時,判斷15s的定時器rotation爲0,則觸發定時器,然後刪除定時器,遍歷到161s定時器時,rotation=1,執行減1,跳過繼續輪轉,當cur_slot=70的時候也就是時間輪走過65*2=130s時,時間輪轉一圈,cur_slot=0,繼續下一圈開始,再走過14*2=28s後,到達15槽,判斷161s定時器,rotation=0,觸發定時器。

有了這些分析,下面直接貼代碼:

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

typedef struct client_data {
    int fd;
    time_t tt;
    char buf[512];
    void* data;
}client_data;
typedef struct tw_timer {
    //處於時間輪第幾轉,即時間輪轉多少轉
    //此定時器可以處於當前轉,若再加上槽
    //即可確定此定時器所處時間輪位置
    int rotation;

    //處於當前時間輪轉的第幾個槽
    int slot;

    //定時器到時執行的回調函數
    void* (*cb_func)( void* param );

    //用戶數據,觸發回調任務函數的參數
    struct client_data c_data;

    //這裏只需要單向不循環鏈表即可
    //struct tw_timer* prev;
    struct tw_timer* next;
}tw_timer;
typedef struct timer_manager {
    //時間輪當前槽,每經過一個間隔時間,加一實現輪轉動,
    //超過總槽數即歸零表示當前輪轉完
    int cur_slot;

    //時間輪一轉的總槽數,總槽數越大槽鏈表越短,效率越高
    int slot_num_r;

    //相鄰時間槽間隔時間,即時間輪轉到下一個槽需要時間,
    //間隔時間越短,精度越高,例如10s,表示定時器支持10s
    //間隔定時器添加,最小支持1s
    int slot_interval;

    //每個時間槽鏈表頭結點,即一個槽管理一條鏈表,鏈表
    //添加相同槽數的結點,但轉數可能不同
    struct tw_timer* slots_head[512];
}timer_manager;

timer_manager tmanager;

void* ontime_func( void* param )
{
    client_data* data = (client_data*)param;
    time_t tt = time(NULL);
    printf("\n----------------------------------------------------\n");
    printf("\tontime,interval:%d\n", (int)(tt - data->tt));
    printf("\told time:%s", ctime(&data->tt));
    printf("\t%s", data->buf);
    printf("\tcur time:%s", ctime(&tt));
    //getchar();
    printf("----------------------------------------------------\n");

    return NULL;
}
int add_timer( timer_manager* tmanager,
    int timeout, client_data* c_data )
{
    if ( timeout < 0 || !tmanager )
        return -1;

    int tick = 0;           //轉動幾個槽觸發
    int rotation = 0;       //處於時間輪第幾轉
    int slot = 0;           //距離當前槽相差幾個槽

    if ( timeout < tmanager->slot_interval )
        tick = 0;
    else
        tick = timeout / tmanager->slot_interval;

    rotation = tick / tmanager->slot_num_r;
    slot = ( tmanager->cur_slot + tick % tmanager->slot_num_r )
                % tmanager->slot_num_r - 1;

    printf("addtimer-->timeout:%d, rotation:%d,slot:%d\n",
        timeout, rotation, slot);

    tw_timer* tmp_t = (tw_timer*)malloc(sizeof(tw_timer));
    tmp_t->rotation = rotation;

    char buf[100] = {0};
    time_t tt = time(NULL) + timeout;

    sprintf( buf, "set time:%s", ctime(&tt));
    memset( tmp_t->c_data.buf, 0, sizeof(tmp_t->c_data.buf));
    strcpy( tmp_t->c_data.buf, buf );
    tmp_t->slot = slot;
    tmp_t->c_data.tt = time(NULL);
    tmp_t->cb_func = ontime_func;

    if ( !tmanager->slots_head[slot] )
    {
        tmanager->slots_head[slot] = tmp_t;
        tmp_t->next = NULL;
        //printf("[line]:%d\n", __LINE__);
        return 0;
    }
    //printf("[line]:%d\n", __LINE__);
    tmp_t->next = tmanager->slots_head[slot]->next;
    tmanager->slots_head[slot]->next = tmp_t;

    return 0;
}
int del_all_timer( timer_manager* tmanager )
{
    //清除、釋放所有定時器,懶得寫了
}
int tick( timer_manager* tmanager )
{
    if ( !tmanager )
        return -1;

    tw_timer* tmp = tmanager->slots_head[tmanager->cur_slot];
    tw_timer* p_tmp;

    while ( tmp )
    {
        //rotation減一,當前時間輪轉不起作用
        //假設這個tmp指向第0個槽的頭,鏈中某個結點的rotaion爲下一圈,
        //即rotation=1,所以這個定時器不起作用,而因爲cur_slot不斷
        //走動,tmp在當前轉不可能再指向這個定時器,下一圈cur_slot
        //爲0時能繼續判斷這個定時器,故實現了定時器處於不同轉的判斷
        if ( tmp->rotation > 0 )
        {
            tmp->rotation--;
            p_tmp = tmp;
            tmp = tmp->next;
        }
        else
        {
            //否則定時器到時,觸發回調函數
            tmp->cb_func( &tmp->c_data );

            //刪除此定時器結點
            //吃了沒用雙向鏈表的虧,寫這麼low
            if ( tmp == tmanager->slots_head[tmanager->cur_slot] )
            {
                //printf("[line]:%d\n", __LINE__);
                tmanager->slots_head[tmanager->cur_slot] = tmp->next;
                p_tmp = tmp;
                tmp = tmp->next;
                free( p_tmp );
                p_tmp = NULL;
                p_tmp = tmp;
                //printf("[line]:%d\n", __LINE__);
            }
            else
            {
                p_tmp->next = p_tmp->next->next;
                free( tmp );
                tmp = NULL;
                tmp = p_tmp->next;
            }
        }
    }
    //更新時間輪,轉動一個槽,轉一圈又從開始轉
    tmanager->cur_slot = ++tmanager->cur_slot % tmanager->slot_num_r;

    return 0;
}
int init_t_manager( timer_manager* tmanager,
    int slot_num_r, int slot_interval )
{
    tmanager->cur_slot = 0;
    tmanager->slot_num_r = slot_num_r;
    tmanager->slot_interval = slot_interval;

    return 0;
}
//自己試着寫的調用層代碼
void alarm_handler( int sig )
{
    time_t tt = time(NULL);
    //printf("timer tick:%s", ctime(&tt));

    int ret = tick( &tmanager );
    if ( ret < 0 )
        printf("tick error\n");

    alarm( tmanager.slot_interval );
}
int main()
{
    time_t tt = time(NULL);

    signal( SIGALRM, alarm_handler );

    //init_t_manager( &tmanager, 60, 10 );
    init_t_manager( &tmanager, 60, 1 );

    add_timer( &tmanager, 6, NULL );
    add_timer( &tmanager, 11, NULL );
    add_timer( &tmanager, 22, NULL );
    add_timer( &tmanager, 33, NULL );
    add_timer( &tmanager, 44, NULL );
    add_timer( &tmanager, 55, NULL );
    add_timer( &tmanager, 66, NULL );
    add_timer( &tmanager, 77, NULL );
    add_timer( &tmanager, 88, NULL );
    add_timer( &tmanager, 99, NULL );
    add_timer( &tmanager, 111, NULL );
    add_timer( &tmanager, 122, NULL );
    add_timer( &tmanager, 133, NULL );
    add_timer( &tmanager, 144, NULL );

    printf("start time:%s\n", ctime(&tt));
    alarm( tmanager.slot_interval );

    while ( 1 )
        sleep( 5 );

    return 0;
}

看以上代碼,main函數開始即指定了SI=1s,N=60,並添加了很多定時器,然後開始以SI執行定時,每一次到時就觸發滴答函數tick(),如此循環定時觸發到時信號就實現了時間輪輪轉。

關於代碼的思考:這裏用了SIGALRM信號,每一次到時,主線程暫停,去執行信號函數內容,如果信號SIGALRM的處理函數太龐大,會影響主線程的任務卡頓,雖然以上代碼執行量不大,但爲了擴展,我覺得可以將定時器觸發執行的操作改爲添加任務結點到任務鏈,這樣配合線程池效率會高一點,線程池本身會從任務鏈取任務結點執行,如果我們的定時處理函數只是往任務鏈放任務,那性能會高很多,而不是往cb_func裏執行具體業務邏輯。

下一篇上時間堆。

發佈了38 篇原創文章 · 獲贊 9 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章