• Linux笔记 高性能定时器


    介绍两种高性能定时器:时间轮和时机堆。

    时间轮

    基于排序链表的定时器,使用一条链表存放所有定时器(时间复杂度O(n)),因此存在添加定时器效率偏低的问题。当存在需要大量添加定时器场景时,添加定时器可能会严重影响性能。

    时间轮可以有效解决这个问题。下图是一个简单时间轮的示意图:

    时机轮内,实线指针指向的一圈(1,2,...,N)代表轮子上的槽(slot)(槽位)。时间轮以恒定速度顺时针转到,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答(tick)。一个滴答的时间称为时间轮的槽间隔si(slot interval),实际上就是心博时间。

    这个时间轮有N个槽位,因此转动一周代表时间Nsi。每个槽位指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差Nsi的整数倍。时间轮利用这个关系将定时器散列到不同链表中:每个定时器有一个超时时间值(int),槽位N个,每个槽位代表si时间,一圈总时间N*si。

    假如当前指针指向cs,用户要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timer slot)对应的链表中:

    ts = (cs + (ti/si)) % N
    

    基于时间轮的定时器,使用哈希表,将定时器散列到不同链表上。这样每条链表上定时器数目明显小于原来排序链表上的定时器数目,插入操作的效率不受定时器数目的影响。

    要提高定时精度,需要槽位间隔时间si值变小;要提高执行效率,则要让N值变大,增多链表数量,减少哈希冲突,避免定时器集中到少数几个链表上。

    复杂时间轮可能有多个轮子,每个轮子有不同粒度(si)。相邻的两个轮子,精度高的转一圈,精度低的转动一个槽位,就像水表。

    实现一个简单时间轮

    定时器tw_timer和用户数据client_data

    一个tw_timer对象代表一个定时器,封装了属于轮子上哪个槽位、定时器超时回调函数。tw_timer以链表形式组织,包含next、prev域名,头节点位于时间轮槽位上,一个槽位就是一个链表头结点。
    由于时间轮转动一圈所代表时间N*si是固定值,所以可以根据用户需要设置的超时时间值,来算出时间轮需要转动的圈数和定时器应该位于时间轮哪个槽位上(即哪个链表上)。

    #define BUFFER_SIZE 64
    class tw_timer;
    
    /* 绑定socket和定时器 */
    struct client_data
    {
        sockaddr_in address;
        int sockfd;
        char buf[BUFFER_SIZE];
        tw_timer* timer;
    };
    
    /* 定时器类 */
    class tw_timer
    {
    public:
        tw_timer(int rot, int ts)
        : next(NULL), prev(NULL), rotation(rot), time_slot(ts)
        {}
    
        int rotation;  /* 记录定时器在时间轮转多少圈后生效 */
        int time_slot; /* 记录定时器属于时间轮上哪个槽位(对应的链表) */
        void (*cb_func)(client_data*); /* 定时器回调 */
        client_data* user_data; /* 客户数据 */
        tw_timer* next; /* 指向下一个定时器 */
        tw_timer* prev; /* 指向钱一个定时器 */
    };
    

    时间轮类time_wheel
    time_wheel有两个重要方法:1)add_timer用于添加定时器;2)del_timer用于删除定时器。

    /* 时间轮类 */
    class time_wheel
    {
    public:
        time_wheel() : cur_slot(0)
        {
            for (int i = 0; i < N; ++i) {
                slots[i] = NULL; /* 初始化每个槽的头节点 */
            }
        }
        ~time_wheel()
        {
            /* 遍历每个槽, 并销毁其中的定时器 */
            for (int i = 0; i < N; ++i) {
                tw_timer* tmp = slots[i];
                while (tmp) {
                    slots[i] = tmp->next;
                    delete tmp;
                    tmp = slots[i];
                }
            }
        }
    
        /* 根据定时值timeout 创建一个定时器, 并把它插入合适的槽中 */
        tw_timer* add_timer(int timeout)
        {
            if (timeout < 0) {
                return NULL;
            }
            int ticks = 0;
            /* 根据带插入定时器的超时值, 计算它将在时间轮转多少个滴答后被触发, 并将该滴答数存储于变量ticks中.
             * 如果待插入定时器的超时值 < 时间轮的槽间隔SI, 则将ticks向上折合为1, 否则就将ticks向下折合为timeout/SI */
            if (timeout < SI) {
                ticks = 1;
            }
            else
            {
                ticks = timeout / SI;
            }
            /* 计算待插入的定时器在时间轮转动多少圈后被触发 */
            int rotation = ticks / N;
            /* 计算待插入的定时器应该被插入哪个槽中 */
            int ts = (cur_slot + (ticks % N)) % N;
            /* 创建新的定时器, 它在时间轮转动rotation圈后被触发, 且位于第ts个槽上 */
            tw_timer* timer = new tw_timer(rotation, ts);
            /* 如果第ts个槽中尚无任何定时器, 则把新建的定时器插入其中, 并将该定时器设置为该槽的头节点 */
            if (!slots[ts]) {
                printf("add timer, rotation is %d, ts is %d, cur_slot is %d\n",
                       rotation, ts, cur_slot);
                slots[ts] = timer;
            }
            /* 否则, 将定时器插入第ts个槽中 */
            else {
                timer->next = slots[ts];
                slots[ts]->prev = timer;
                slots[ts] = timer;
            }
            return timer;
        }
    
        /* 删除目标定时器timer */
        void del_timer(tw_timer* timer)
        {
            if (!timer) {
                return;
            }
            int ts = timer->time_slot;
            /* slots[ts]是目标定时器所在槽的头节点. 如果目标定时器就是该头节点,
             * 则需要重置第ts个槽的头节点 */
            if (timer == slots[ts]) {
                slots[ts] = slots[ts]->next;
                if (slots[ts]) {
                    slots[ts]->prev = NULL;
                }
                delete timer;
            }
            else {
                timer->prev->next = timer->next;
                if (timer->next) {
                    timer->next->prev = timer->prev;
                }
                delete timer;
            }
        }
    
        /* SI时间到后, 调用该函数, 时间轮向前滚动一个槽的间隔.
         * 要求每隔SI时间, 定时调用一次该函数 */
        void tick()
        {
            tw_timer* tmp = slots[cur_slot]; /* 取得时间轮上当前槽的头节点 */
            printf("current slot is %d\n", cur_slot);
            /* 遍历当前槽位对应链表 */
            while (tmp) {
                printf("tick the timer once\n");
                /* 如果定时器的rotation值 > 0, 则它在这一轮不起作用 */
                if (tmp->rotation > 0) {
                    tmp->rotation--;
                    tmp = tmp->next;
                }
                /* 否则, 说明定时器已经到期, 于是执行超时任务, 然后删除该定时器 */
                else {
                    tmp->cb_func(tmp->user_data);
                    if (tmp == slots[cur_slot]) { // 超时定时器就是当前槽位头节点, 删除时要专门处理
                        printf("delete header in cur_slot\n");
                        slots[cur_slot] = tmp->next;
                        delete tmp;
                        if (slots[cur_slot]) {
                            slots[cur_slot]->prev = NULL;
                        }
                        tmp = slots[cur_slot];
                    }
                    else { // 超时定时器不是当前槽位头节点
                        tmp->prev->next = tmp->next;
                        if (tmp->next) {
                            tmp->next->prev = tmp->prev;
                        }
                        tw_timer* tmp2 = tmp->next;
                        delete tmp;
                        tmp = tmp2;
                    }
                }
            }
            cur_slot = ++cur_slot % N; /* 更新时间轮当前槽位, 以反映时间轮的转动 */
        }
    
    private:
        /* 时间轮上槽的数目 */
        static const int N = 60;
        /* 每1秒时间轮转动一次, 即槽间隔1s */
        static const int SI = 1;
        /* 时间轮的槽, 其中每个元素指向一个定时器链表, 链表无序 */
        tw_timer* slots[N];
        int cur_slot; /* 时间轮的当前槽 */
    };
    

    使用时间轮

    借用muduo函数的EventLoop::runEvery,为心博函数time_wheel::tick()提供1秒周期的入口。

    #include "time_wheel.h"
    #include "muduo/net/EventLoop.h"
    #include <stdio.h>
    
    using namespace muduo;
    using namespace muduo::net;
    
    void print(client_data* data)
    {
        printf("%s\n", data->buf);
    }
    
    int main()
    {
        time_wheel wheel;
        tw_timer* timer = wheel.add_timer(5); // 添加5秒后运行的定时器
        timer->cb_func = print;
        timer->user_data = new client_data;
        strncpy(timer->user_data->buf, "hello", strlen("hello"));
    
        EventLoop loop;
        loop.runEvery(1.0, std::bind(&time_wheel::tick, &wheel)); // 每1秒钟运行1次心博函数tick()
        loop.loop();
        return 0;
    }
    

    时间堆

    时间轮是以固定频率调用心搏函数tick,在其中依次查找到期(轮数+槽位 确定定时器超时时间)的定时器,然后执行定时器超时回调函数。
    另一种设计定时器思路:将所有定时器中超时时间最小的一个定时器的超时值,作为心博间隔。这样,一旦心博函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick()中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的那个,并将这段最小时间设为下一次心博间隔。如此反复,实现较为精准的定时。

    最小堆用来处理这种定时方案。

    什么是最小堆?

    最小堆(又称小根堆)是指具有这种特性的一颗完全二叉树:每个节点的值 <= 子节点的值。

    如下图所示

    比如,节点21,21 <= 24且21<=31;节点24 <= 65且24 <= 26;节点31 <= 32。

    树基本操作:插入节点、删除节点。

    插入操作

    为了将元素X插入最小堆,可以在树下一个空闲位置创建一个空穴(没有元素的节点)。如果X可以放在空穴中而不被破坏堆特性,则插入完成;否则,违法了对特性,执行上虑操作,即交换空穴和其父节点元素,直到X可以被放入空穴。这个过程也称为向上调整堆。

    例如,要往上图所示最小堆插入值14,可以按下图所示步骤:

    删除操作

    删除根节点元素,并且不破坏堆特性。删除时,需要先在根节点创建一个空穴。由于堆少了一个元素,不符合堆特性,因此可以把堆最后一个元素X移动到该堆的某个地方。如果X可以被放入空穴,则删除操作完成;否则,就指向下虑操作,即交换空穴和它的两个儿子节点中较小者。不断进行上述过程,直到X可以被放入空穴。这个过程也称为向下调整堆。

    例如,要往上面图示最小堆执行删除(堆顶)操作,可以按下图所示步骤进行:

    因为最小堆是一种完全二叉树,因此可以用数组来存储其元素。对于数组中任意位置i上的元素,其左儿子节点在位置2i + 1,右儿子节点在位置2i + 2,父节点在[(i-1) / 2] (i > 0)。与用链表表示堆相比,用数组表示堆节省空间,而且更容易实现堆的插入、删除等操作(数组支持随机访问)。

    对时间堆,添加一个定时器时间复杂度:O(logn);删除一个定时器时间复杂度:O(1);执行一个定时器时间复杂度:O(1)。

    用最小堆实现时间堆

    用最小堆实现的定时器称为时间堆。

    用户数据类client_data 和 定时器类heap_timer

    #define BUFFER_SIZE 64
    
    class heap_timer; /* 前向声明 */
    /* 绑定socket和定时器 */
    struct client_data
    {
        sockaddr_in address;
        int sockfd;
        char buf[BUFFER_SIZE];
        heap_timer* timer;
    };
    
    /* 定时器类 */
    class heap_timer
    {
    public:
        explicit heap_timer(int delay)
        {
            expire = time(NULL) + delay;
        }
        time_t expire; /* 定时器生效的绝对时间 */
        void (*cb_func)(client_data*); /* 定时器的回调函数 */
        client_data* user_data; /* 用户数据 */
    };
    

    时间堆类time_heap

    #include <iostream>
    #include <netinet/in.h>
    #include <time.h>
    using std::exception;
    
    /* 时间堆类 */
    class time_heap
    {
    public:
        /* 构造函数之一, 初始化一个大小为cap的空堆 */
        time_heap(int cap) throw(std::exception) : capacity(cap), cur_size(0)
        {
            array = new heap_timer*[capacity]; /* 创建堆数组 */
            if (!array) {
                throw std::exception();
            }
            for (int i = 0; i < capacity; ++i) {
                array[i] = NULL;
            }
        }
        /* 构造函数之二, 用已有数组来初始化堆 */
        time_heap(heap_timer** init_array, int size, int cap) throw(std::exception)
        : cur_size(size), capacity(cap)
        {
            if (capacity < size) {
                throw std::exception();
            }
            array = new heap_timer*[capacity]; /* 创建堆数组 */
            if (!array) {
                throw std::exception();
            }
            for (int i = 0; i < capacity; ++i) {
                array[i] = NULL;
            }
            if (size != 0) {
                /* 初始化堆数组 */
                for (int i = 0; i < size; ++i) {
                    array[i] = init_array[i];
                }
                for (int i = (cur_size - 1) / 2; i >= 0 ; ++i) {
                    /* 对数组中第[(cur_size - 1)/2]~0个元素执行下虑操作 */
                    percolate_down(i);
                }
            }
        }
    
        /* 销毁时间堆 */
        ~time_heap()
        {
            for (int i = 0; i < cur_size; ++i) {
                delete array[i];
            }
            delete[] array;
        }
    
        /* 添加目标定时器timer */
        void add_timer(heap_timer* timer) throw(std::exception)
        {
            if (!timer) {
                return;
            }
            if (cur_size >= capacity) { /* 如果当前数组容量不够, 则将其扩大一倍 */
                resize();
            }
            /* 新插入一个元素, 当前堆大小+1, hole是新建空穴的位置 */
            int hole = cur_size++;
            int parent = 0;
            /* 对从空穴到根节点的路径上的所有节点执行上虑操作 */
            for ( ; hole > 0; hole = parent) {
                parent = (hole - 1) / 2;
                if (array[parent]->expire <= timer->expire) {
                    break;
                }
                array[hole] = array[parent];
            }
            array[hole] = timer;
        }
    
        /* 删除目标定时器timer */
        void del_timer(heap_timer* timer)
        {
            if (!timer) {
                return;
            }
            /* 仅仅将目标定时器的回调函数设置为空, 即所谓的延迟销毁.
             * 将节省真正删除该定时器造成的开销, 但这样容易使堆数组膨胀 */
            // FIXME: 删除定时器, 为何不重新调整堆?
            timer->cb_func = NULL;
        }
    
        /* 获得堆顶部的定时器 */
        heap_timer* top() const
        {
            if (empty()) {
                return NULL;
            }
            return array[0];
        }
    
        /* 删除堆顶部定时器 */
        void pop_timer()
        {
            if (empty()) {
                return;
            }
            if (array[0]) {
                delete array[0];
                /* 将原来堆顶元素替换为堆数组中最后一个元素 */
                array[0] = array[--cur_size];
                percolate_down(0); /* 对新的堆顶元素执行下虑操作 */
            }
        }
    
        /* 心搏函数 */
        void tick()
        {
            heap_timer* tmp = array[0];
            time_t cur = time(NULL); /* 循环处理堆中到期的定时器 */
            while (!empty()) {
                if (!tmp) {
                    break;
                }
                /* 如果堆顶定时器没到期, 则退出循环 */
                if (tmp->expire > cur) {
                    break;
                }
                /* 否则执行堆顶定时器中的任务 */
                if (array[0]->cb_func) {
                    array[0]->cb_func(array[0]->user_data);
                }
                /* 将堆顶元素删除, 同时生成新的堆顶定时器(array[0]) */
                pop_timer();
                tmp = array[0];
            }
        }
    
        bool empty() const
        { return cur_size == 0; }
    
    private:
        /* 最小堆的下虑操作, 它确保堆数组中以第hole个节点作为根的子树拥有最小堆性质 */
        void percolate_down(int hole)
        {
            heap_timer* temp = array[hole];
            int child = 0;
            for ( ; (hole * 2 + 1) <= (cur_size - 1); hole = child) {
                child = hole * 2 + 1;
                /* 找到hole子节点中最小节点child */
                if ((child < (cur_size - 1)) && (array[child + 1]->expire < array[child]->expire)) {
                    ++child;
                }
                /* 交换hole, child位置对应元素, 以满足小堆特性 */
                if (array[child]->expire < temp->expire) {
                    array[hole] = array[child];
                }
                /* 否则, 直接跳出继续找子节点的循环. 因为子树中不会存在不满足对特性的节点了 */
                else {
                    break;
                }
            }
            /* 找到hole最终位置, 并赋值 */
            array[hole] = temp;
        }
    
        /* 将堆数组容量扩大1倍 */
        void resize() throw(std::exception)
        {
            heap_timer** temp = new heap_timer*[2 * capacity];
            for (int i = 0; i < 2 * capacity; ++i) {
                temp[i] = NULL;
            }
            if (!temp) {
                throw std::exception();
            }
            capacity = 2 * capacity;
            for (int i = 0; i < cur_size; ++i) {
                temp[i] = array[i];
            }
            delete[] array;
            array = temp;
        }
    
    private:
        heap_timer** array; /* 堆数组 */
        int capacity; /* 堆数组容量 */
        int cur_size; /* 堆数组当前包含元素的个数 */
    };
    

    时间堆类的使用

    提供time_heap::tick()的入口时间,不必是固定频率,只需要 <= 当前堆顶定时器的超时时间即可。

    #include "time_heap.h"
    #include <thread>
    using namespace std;
    
    void print(client_data* data)
    {
        printf("%s\n", data->buf);
    }
    
    int main()
    {
        time_heap th(20);
        for (int i = 8; i >= 0; --i) {
            int delay = i + 1;
            heap_timer* timer = new heap_timer(delay);
            timer->user_data = new client_data;
            snprintf(timer->user_data->buf, sizeof(timer->user_data->buf), "runs at %d sec", delay);
            timer->cb_func = print;
            th.add_timer(timer);
        }
    
        while (1) {
            th.tick();
            this_thread::sleep_for(chrono::milliseconds(100));
        }
        return 0;
    }
    

    参考

    《Linux高性能服务器编程》

  • 相关阅读:
    config https in nginx(free)
    js hex string to unicode string
    alter character set
    es6
    音乐播放器
    JS模块化-requireJS
    PHP中的封装和继承
    JavaScriptOOP
    mui框架移动开发初体验
    走进AngularJS
  • 原文地址:https://www.cnblogs.com/fortunely/p/16210962.html
Copyright © 2020-2023  润新知