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高性能服务器编程》

posted @ 2022-04-30 18:59  明明1109  阅读(586)  评论(0编辑  收藏  举报