高性能组件(1)定时器-难点

本文将介绍服务器中经常使用的定时器方案:

业务场景:
什么时候用定时器?
1、心跳检测
2、游戏中的技能冷却

定时器的实现方式:
1、在nginx和redis当中,将网络事件和时间事件放在一个线程中处理:
通过epoll_Wait函数,设置最后参数timeout = 最近任务的时间 - 当前时间,即可实现定时器。在timeout时间内,线程会阻塞。时间到了,获取相关数据进行处理;

// 第⼀种
while (!quit) {
 int now = get_now_time();// 单位:ms
 int timeout = get_nearest_timer() - now;
 if (timeout < 0) timeout = 0;
 int nevent = epoll_wait(epfd, ev, nev, timeout);
 for (int i=0; i<nevent; i++) {
 //... ⽹络事件处理
 }
 update_timer(); // 时间事件处理
}

2、skynet中,网络事件和时间事件在不同的线程处理:
另外开启个线程,线程中调用sleep函数来进行阻塞,然后将定时事件任务发送到消息队列中,其他流程会从消息队列中拿任务进行处理。

// 第⼆种 在其他线程添加定时任务
void* thread_timer(void * thread_param) {
 init_timer();
 while (!quit) {
 update_timer(); // 更新检测定时器,并把定时事件发送到消息队列中
 sleep(t); // 这⾥的 t 要⼩于 时间精度
 }
 clear_timer();
 return NULL;
}

想要使用定时器,需要对外提供哪些接口呢?
1、初始化定时器
2、添加定时器
3、删除定时器
4、更新检测定时器
5、找到最近要发生的定时器

定时器可以有哪些数据结构实现?
有:红黑树、最小堆、跳表和时间轮。其中红黑树、最小堆和跳表的有序性是和定时任务的时间有关系的,而时间轮没有。
这些结构都保证了定时器是个有序的结构,且能够快速找到最小节点。

红黑树:
注意:红黑树是key-value的结构,且key是唯一的。但是业务上有同时大量的时间任务要处理,红黑树这种结构要怎么处理呢?
答:在插入的时候,判断key值大小时,待插的key时间小于当前节点的时间时,就往左插;大于或等于的,就往右插。

最小堆:
特征:
1、是一个完全二叉树:二叉树的深度为h,其他层的节点数都是该层的容量最大数2^n,且h层的节点都集中在最左侧;
2、某节点的值小于或等于其子节点,子节点之间大小没有限制;
3、堆中每个节点的子树都是最小堆;

注意:这里很多是最小堆的删除等操作,记得及时复习。面试要求手写的话,答这个,这个手写是最简单的。
定时器节点:

struct TimerNode { //定时器的节点
    int idx = 0;
    int id = 0;
    unsigned int expire = 0;
    TimerHandler cb = NULL;
};

最小堆定时器

class MinHeapTimer { //最小堆的定时器实现
public:
    MinHeapTimer() {
        _heap.clear();
        _map.clear();
    }
    static inline int Count() {
        return ++_count;
    }

    int AddTimer(uint32_t expire, TimerHandler cb);//添加定时器
    bool DelTimer(int id); //删除节点
    void ExpireTimer(); //执行最近时间任务

private:
    inline bool _lessThan(int lhs, int rhs) {
        return _heap[lhs]->expire < _heap[rhs]->expire;
    }
    bool _shiftDown(int pos);
    void _shiftUp(int pos);
    void _delNode(TimerNode *node);

private:
    vector<TimerNode*>  _heap; //动态的数组,自动扩容
    map<int, TimerNode*> _map; //最小堆删除节点比较麻烦,所以用map先快速找到节点再删除
    static int _count;
};

添加定时器任务:

//添加定时器
int MinHeapTimer::AddTimer(uint32_t expire, TimerHandler cb) {
    int64_t timeout = current_time() + expire;
    TimerNode* node = new TimerNode;
    int id = Count();
    node->id = id;
    node->expire = timeout;
    node->cb = cb;
    node->idx = (int)_heap.size(); //节点的初始化
    _heap.push_back(node); //将节点放置在数组的最后面
    _shiftUp((int)_heap.size() - 1); //上升操作
    _map.insert(make_pair(id, node));
    return id;
}

上升操作:

void MinHeapTimer::_shiftUp(int pos) //上升操作
{
    for (;;) {
        int parent = (pos - 1) / 2; // parent node 找到父亲节点
        if (parent == pos || !_lessThan(pos, parent)) {
            break; //若当前位置小于父亲
        }
        std::swap(_heap[parent], _heap[pos]); //就和父亲位置进行调换
        _heap[parent]->idx = parent;
        _heap[pos]->idx = pos;
        pos = parent; //然后将值进行相应的替换
    }
}

删除定时器任务:

bool MinHeapTimer::DelTimer(int id) //删除节点
{
    auto iter = _map.find(id);
    if (iter == _map.end())
        return false; //快速找到对应的节点
    _delNode(iter->second); //删除节点
    return true;
}

void MinHeapTimer::_delNode(TimerNode *node)
{
	//找到指定的节点;与最后的节点调换;尝试下沉操作,与子节点比较
    int last = (int)_heap.size() - 1; //先找到最后一个节点
    int idx = node->idx;
    if (idx != last) { //若当前节点不是最后一个节点,就二者调换
        std::swap(_heap[idx], _heap[last]);
        _heap[idx]->idx = idx;
        if (!_shiftDown(idx)) { //先尝试下沉,下沉失败就上升
            _shiftUp(idx);
        }
    }
    _heap.pop_back(); //上升失败就把刚开始最后一个节点删除,要满足删除节点后仍要满足最小堆
    _map.erase(node->id);
    delete node;
}

下沉操作:

bool MinHeapTimer::_shiftDown(int pos){
    int last = (int)_heap.size()-1;
    int idx = pos;
    for (;;) {
        int left = 2 * idx + 1;
        if ((left >= last) || (left < 0)) {
            break;
        }
        int min = left; // left child
        int right = left + 1;
        if (right < last && !_lessThan(left, right)) {
            min = right; // right child
        }
        if (!_lessThan(min, idx)) {
            break;
        }
        std::swap(_heap[idx], _heap[min]);
        _heap[idx]->idx = idx;
        _heap[min]->idx = min;
        idx = min;
    }
    return idx > pos;
}

执行最近时间任务:

//找到最近时间的定时任务执行
void MinHeapTimer::ExpireTimer()
{
    if (_heap.empty()) return;
    uint32_t now = current_time();
    do {
        TimerNode* node = _heap.front(); //找最小节点,最小节点就是根节点
        if (now < node->expire)
            break;
		//若根节点都小于=现有节点时间戳了,就触发定时任务,说明必有节点被触发了
        for (int i = 0; i < _heap.size();  i++)
            std::cout << "touch    idx: " << _heap[i]->idx 
                << " id: " << _heap[i]->id << " expire: "
                << _heap[i]->expire << std::endl;
        if (node->cb) {
            node->cb(node); //执行定时任务
        }
        _delNode(node); //然后删除节点 
    } while(!_heap.empty());
}

时间轮:
为了便于理解,先讲单层时间轮
背景:
客户但每5s发送一次线条包,服务端每10s检测一次心跳数据,若没有检测到就删除连接;若有大量的连接(以万为单位)都采用map结构来存储连接的话,每次检测都会做很多无效检测。我们采用时间轮来做:
大体思路是将设置一个数组存储连接数,数组大小必须大于检测时间10s.我们将连接数据插到数组的计算方法是:( 当前时间+检测时间(10s) ) % 数组长度。
基本上就是
m % n = m & ( (2^k) - 1 );
将n替换为2^k,所以我们选16(2的四次方)。
因为是10s内检测心跳包,而我们是5s发一次数据,所以10s内可能检测到两次心跳包,要有一个used引用计数。收到心跳包就used+1,检测一次就used-1,used==0时,就踢掉连接。
若⼀秒内添加了多条连接,那么可以参考 hash 结构处理冲突的⽅式,⽤链表链接起来;

数组下的linklist

//为了解决 频繁创建销毁内存,开源框架很多都是直接声明个静态结构
static timer_node_t timer_nodes[MAX_TIMER] = {0};
static conn_node_t conn_nodes[MAX_CONN] = {0};
static uint32_t t_iter = 0; //这个指针可从timer_nodes中获取个有用的节点
static uint32_t c_iter = 0;

//数组格子,每个格子就是一个linklist
typedef struct link_list {
	timer_node_t head;
	timer_node_t *tail; //便于快速增加节点
}link_list_t;

typedef struct conn_node {
    uint8_t used; //引用指针,检测一次就--,==0就释放掉节点
    int id; //fd
} conn_node_t;

添加连接:

//添加连接,
void add_conn(link_list_t *tw, conn_node_t *cnode, int delay) {
    //EXPIRE=10 tick为当前的位置,取出对应的格子
	//delay是告诉在哪里的时候添加心跳包,不能在定时任务加,所以这里加
	link_list_t *list = &tw[(tick+EXPIRE+delay) & TW_MASK];
    timer_node_t * tnode = get_timer_node();
    cnode->used++;
    tnode->node = cnode;
    list->tail->next = tnode;
	list->tail = tnode;
	tnode->next = NULL;
}

timer_node_t * get_timer_node() { // 注意:没有检测定时任务数超过 MAX_TIMER 的情况
    t_iter++;
    while (timer_nodes[t_iter & MAX_TIMER].idx > 0) {
        t_iter++;
    }
    timer_nodes[t_iter].idx = t_iter;
    return &timer_nodes[t_iter];
}

检测某个槽位的链表:

void check_conn(link_list_t *tw) {
    int32_t itick = tick;
    tick++;
    link_list_t *list = &tw[itick & TW_MASK]; //就取出这个槽位下的链表
    timer_node_t *current = list->head.next; //拿到第一个链表,慢慢往下遍历
    while (current) {
		timer_node_t * temp = current;
		current = current->next; 
        conn_node_t *cn = temp->node;
        cn->used--; //检测的节点引用计数-1
        temp->idx = 0;
        if (cn->used == 0) { //准备closefd
            printf("fd:%d kill down\n", cn->id);
            temp->next = NULL;
            continue;
        }
        printf("fd:%d used:%d\n", cn->id, cn->used);
	} 
    link_clear(list); //每次检测一条链表都将其清空 
}

void link_clear(link_list_t *list) {
	list->head.next = NULL;
	list->tail = &(list->head); //清空了尾指针指向头部
}

多层级时间轮
底层运转一圈,中层移动一格,中层移动一圈,高层移动一格。
层级1:移动一格时将其中链表的数据去执行
层级2:移动一格将链表重新映射到底层1;

添加节点

void add_node(timer_t *T, timer_node_t *node) {
 uint32_t time=node->expire;
 uint32_t current_time=T->time;
 uint32_t msec = time - current_time;
 if (msec < TIME_NEAR) { //[0, 0x100)
 // time % 256
 link(&T->near[time&TIME_NEAR_MASK],node);
 } else if (msec < (1 << (TIME_NEAR_SHIFT+TIME_LEVEL_SHIFT)))
{//[0x100, 0x4000)
 // floor(time/2^8) % 64
 link(&T->t[0][((time>>TIME_NEAR_SHIFT) & TIME_LEVEL_MASK)],node);
  } else if (msec < (1 << (TIME_NEAR_SHIFT+2*TIME_LEVEL_SHIFT)))
{//[0x4000, 0x100000)
 // floor(time/2^14) % 64
 link(&T->t[1][((time>>(TIME_NEAR_SHIFT + TIME_LEVEL_SHIFT)) &
TIME_LEVEL_MASK)],node); 
 } else if (msec < (1 << (TIME_NEAR_SHIFT+3*TIME_LEVEL_SHIFT)))
{//[0x100000, 0x4000000)
 // floor(time/2^20) % 64
 link(&T->t[2][((time>>(TIME_NEAR_SHIFT + 2*TIME_LEVEL_SHIFT)) &
TIME_LEVEL_MASK)],node); 
 } else {//[0x4000000, 0xffffffff]
 // floor(time/2^26) % 64
 link(&T->t[3][((time>>(TIME_NEAR_SHIFT + 3*TIME_LEVEL_SHIFT)) &
TIME_LEVEL_MASK)],node); 
 }
}

重新映射:

void
timer_shift(timer_t *T) {
 int mask = TIME_NEAR;
 uint32_t ct = ++T->time; // 第⼀层级指针移动 ++ ⼀次代表10ms
 if (ct == 0) {
 move_list(T, 3, 0);
 } else {
 // floor(ct / 256)
 uint32_t time = ct >> TIME_NEAR_SHIFT;
 int i=0;
 // ct % 256 == 0 说明是否移动到了 不同层级的 最后⼀格
 while ((ct & (mask-1))==0) {
 int idx=time & TIME_LEVEL_MASK;
 if (idx!=0) {
 move_list(T, i, idx); // 这⾥发⽣重新映射,将i+1层级idx格⼦中的
定时任务重新映射到i层级中
 }
 mask <<= TIME_LEVEL_SHIFT;
 time >>= TIME_LEVEL_SHIFT;
 ++i;
 }
 }
}

面试题:
1、定时器有哪些数据结构可以实现?
2、为什么这些数据结构可以实现
3、手写定时器,写最小堆或者单层级的时间轮

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值