服务端在一下业务场景下,需要用到定时器
1.⼼跳检测2.技能冷却3.武器冷却4.倒计时5.其它需要使⽤超时机制的功能
对于服务端来说,驱动服务端逻辑的事件主要有两个,一个是网络事件,另一个是时间事件。对这两种事件的处理有两种方式:1.在主线程处理网络事件时,处理时间事件,例如ngnix,redis2.网络事件和时间事件在不同的线程中处理,例如,skynet
第一种模式示例代码如下:
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(); // 时间事件处理
}
第二种模式示例代码如下:
// 第⼆种 在其他线程添加定时任务
void* thread_timer(void * thread_param) {
init_timer();
while (!quit) {
update_timer(); // 更新检测定时器,并把定时事件发送到消息队列中
sleep(t); // 这⾥的 t 要⼩于 时间精度
}
clear_timer();
return NULL; }
pthread_create(&pid, NULL, thread_timer, &thread_param);
定时器的设计常用接口如下示例代码:
// 初始化定时器
void init_timer();
// 添加定时器
Node* add_timer(int expire, callback cb);
// 删除定时器
bool del_timer(Node* node);
// 找到最近要发⽣的定时任务
Node* find_nearest_timer();
// 更新检测定时器
void update_timer();
对定时器进行存储和维护的常用数据结构有以下几种:
第一,用红黑树存储定时器:
首先,红黑树对于增删查,时间复杂度为 O(log2n),对于红⿊树最⼩节点为最左侧节点,时间复杂度为O(log2n)
红黑树定时任务添加的关键代码如下:
void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel) { ngx_rbtree_node_t **p; for ( ;; ) { // 这⾥是重点 p = ((ngx_rbtree_key_int_t) (node->key - temp->key) < 0) ? &temp->left : &temp->right; if (*p == sentinel) { break; } temp = *p; } *p = node; node->parent = temp; node->left = sentinel; node->right = sentinel; ngx_rbt_red(node); }
STL 中 map 结构采⽤的是红⿊树来实现,但是定时器不要使⽤ map 结构来实现,因为可能多个定 时任务需要同时被触发, map 中的 key是惟⼀的; 红⿊树的节点同时包含 key 和 val ,红⿊树节点的有序由 key 来决定的;插⼊节点的时候,通 过 ⽐较 key 来决定节点存储位置;红⿊树的实现并没有要求 key 唯⼀;如上代码示例, for 循环 中 (node->key - temp->key) < 0)? &temp->left : &temp->right; 当 key相同的时候取值 为 temp->right ;思考: map 结构中插⼊节点这⾥如何操作?
第二种,利用最小堆来存储和管理时间事件
对于最小堆的理解,需要先理解以下数据结构
满⼆叉树 :所有的层 节点数 都是该层所能容纳节点的最⼤数量(满⾜ 2的n次方,n >= 0);完全⼆叉树 :若⼆叉树的深度为 h ,除了 h 层外,其他层的 节点数都是该层所能容纳节点的最⼤ 数量(满⾜ 2的n次方 ,n >= 0),且 h 层都集中在最左侧;最小堆需要满足以下三点:1. 是⼀颗完全⼆叉树;2. 某⼀个节点的值总是⼩于等于它的⼦节点的值;3. 堆中每个节点的⼦树都是最⼩堆;如下图:最小堆的增加和删除操作步骤如下:增加操作为了满⾜完全⼆叉树定义,往⼆叉树最⾼层沿着最左侧添加⼀个节点;然后考虑是否能上升操作; 如果此时添加值为 4 的节点, 4 节点是 5 节点的左⼦树; 4 ⽐ 5 ⼩, 4 和 5 需要交换位置;删除操作删除操作需要先查找是否包含这个节点,最⼩堆的查找效率是 O(n);查找之后,交换最后⼀个节 点,先考虑下降操作,如果操作失败则上升操作;最后删除最后⼀个节点; 例如:假设删除 1 号节点,则需要下沉操作;假设删除 9 号节点,则需要上升操作;最小堆示例代码如下:头文件代码:#pragma once #include <vector> #include <map> using namespace std; typedef void (*TimerHandler) (struct TimerNode *node); 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; static int _count; }; int MinHeapTimer::_count = 0;
源文件代码如下:
#include <unistd.h> #if defined(__APPLE__) #include <AvailabilityMacros.h> #include <sys/time.h> #include <mach/task.h> #include <mach/mach.h> #else #include <time.h> #endif #include <iostream> #include "minheap.h" static uint32_t current_time() { uint32_t t; #if !defined(__APPLE__) || defined(AVAILABLE_MAC_OS_X_VERSION_10_12_AND_LATER) struct timespec ti; clock_gettime(CLOCK_MONOTONIC, &ti); t = (uint32_t)ti.tv_sec * 1000; t += ti.tv_nsec / 1000000; #else struct timeval tv; gettimeofday(&tv, NULL); t = (uint32_t)tv.tv_sec * 1000; t += tv.tv_usec / 1000; #endif return t; } 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; } 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; } 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()); } 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::_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; } } void print_hello(TimerNode *te) { std::cout << "hello world time = " << te->idx << "\t" << te->id << std::endl; } int main() { MinHeapTimer mht; mht.AddTimer(0, print_hello); mht.AddTimer(1000, print_hello); mht.AddTimer(7000, print_hello); mht.AddTimer(2000, print_hello); mht.AddTimer(9000, print_hello); mht.AddTimer(10000, print_hello); mht.AddTimer(6000, print_hello); mht.AddTimer(3000, print_hello); for (;;) { mht.ExpireTimer(); usleep(10000); } return 0; }
第三种定时器处理数据结构:时间轮
时间轮概念:从时钟表盘出发,如何⽤数据结构来描述秒表的运转; int seconds[60]; // 数组来描述表盘刻度;++tick% 60 ;每秒钟 ++tick 来描述秒针移动;对tick % 60让秒表永远在[0,59]区间移动;对于时钟来说,它的时间精度(最⼩运⾏单元)是 1 秒;先了解单层级时间轮单层级时间轮背景⼼跳检测:客户端每 5 秒钟发送⼼跳包;服务端若 10 秒内没收到⼼跳数据,则清除连接;实际在开发过程中,若收到除了⼼跳包的其他数据,⼼跳检测也算通过,在这⾥为了简化流程,只判断⼼跳包;作为对⽐:我们假设使⽤ map<int, conn*> 来存储所有连接数;每秒检测 map 结构,那么每秒需要遍历所有的连接,如果这个 map 结构包含⼏万条连接,那么我们做了很多⽆效检测;考虑极端情况,刚添加进来的连接,下⼀秒就需要去检测,实际上只需要 10 秒后检测就⾏了;那么我们考虑使⽤时间轮来检测;注意 :这个例⼦只是⽤来帮助理解时间轮,不代表实际解决⽅案再来了解多层级时间轮多层级时间轮背景参照时钟表盘的运转规律,可以将定时任务根据触发的紧急程度,分布到不同层级的时间轮中;假设时间精度为 10ms ;在第 1 层级每 10ms 移动⼀格;每移动⼀格执⾏该格⼦当中所有的定时任务;当第 1 层指针从 255 格开始移动,此时层级 2 移动⼀格;层级 2 移动⼀格的⾏为定义为,将该格当中的定时任务重新映射到层级 1 当中;同理,层级 2 当中从 63 格开始移动,层级 3 格⼦中的定时任务重新映射到层级 2 ; 以此类推层级 4 往层级 3 映射,层级 5 往层级 4 映射;如何重新映射?定时任务的过期时间对上⼀层级的⻓度取余分布在上⼀层级不同格⼦当中;头文件示例代码如下:#ifndef _MARK_TIMEWHEEL_ #define _MARK_TIMEWHEEL_ #include <stdint.h> #define TIME_NEAR_SHIFT 8 #define TIME_NEAR (1 << TIME_NEAR_SHIFT) #define TIME_LEVEL_SHIFT 6 #define TIME_LEVEL (1 << TIME_LEVEL_SHIFT) #define TIME_NEAR_MASK (TIME_NEAR-1) #define TIME_LEVEL_MASK (TIME_LEVEL-1) typedef struct timer_node timer_node_t; typedef void (*handler_pt) (struct timer_node *node); struct timer_node { struct timer_node *next; uint32_t expire; handler_pt callback; uint8_t cancel; int id; // 此时携带参数 }; timer_node_t* add_timer(int time, handler_pt func); void expire_timer(void); void del_timer(timer_node_t* node); void init_timer(void); #endif
源文件示例代码如下:
#include "spinlock.h" #include "timewheel.h" #include <string.h> #include <stddef.h> #include <stdlib.h> #if defined(__APPLE__) #include <AvailabilityMacros.h> #include <sys/time.h> #include <mach/task.h> #include <mach/mach.h> #else #include <time.h> #endif typedef struct link_list { timer_node_t head; timer_node_t *tail; }link_list_t; typedef struct timer { link_list_t near[TIME_NEAR]; link_list_t t[4][TIME_LEVEL]; struct spinlock lock; uint32_t time; uint64_t current; uint64_t current_point; }timer_t; static timer_t * TI = NULL; timer_node_t * link_clear(link_list_t *list) { timer_node_t * ret = list->head.next; list->head.next = 0; list->tail = &(list->head); return ret; } void link(link_list_t *list, timer_node_t *node) { list->tail->next = node; list->tail = node; node->next=0; } 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) link(&T->near[time&TIME_NEAR_MASK],node); } else if (msec < (1 << (TIME_NEAR_SHIFT+TIME_LEVEL_SHIFT))) {//[0x100, 0x4000) 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) 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) link(&T->t[2][((time>>(TIME_NEAR_SHIFT + 2*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node); } else {//[0x4000000, 0xffffffff] link(&T->t[3][((time>>(TIME_NEAR_SHIFT + 3*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node); } } timer_node_t* add_timer(int time, handler_pt func) { timer_node_t *node = (timer_node_t *)malloc(sizeof(*node)); spinlock_lock(&TI->lock); node->expire=time+TI->time;// 每10ms加1 0 node->callback = func; if (time <= 0) { node->callback(node); free(node); return NULL; } add_node(TI, node); spinlock_unlock(&TI->lock); return node; } void move_list(timer_t *T, int level, int idx) { timer_node_t *current = link_clear(&T->t[level][idx]); while (current) { timer_node_t *temp=current->next; add_node(T,current); current=temp; } } void timer_shift(timer_t *T) { int mask = TIME_NEAR; uint32_t ct = ++T->time; if (ct == 0) { move_list(T, 3, 0); } else { // 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); break; } mask <<= TIME_LEVEL_SHIFT; time >>= TIME_LEVEL_SHIFT; ++i; } } } void dispatch_list(timer_node_t *current) { do { timer_node_t * temp = current; current=current->next; if (temp->cancel == 0) temp->callback(temp); free(temp); } while (current); } void timer_execute(timer_t *T) { int idx = T->time & TIME_NEAR_MASK; while (T->near[idx].head.next) { timer_node_t *current = link_clear(&T->near[idx]); spinlock_unlock(&T->lock); dispatch_list(current); spinlock_lock(&T->lock); } } void timer_update(timer_t *T) { spinlock_lock(&T->lock); timer_execute(T); timer_shift(T); timer_execute(T); spinlock_unlock(&T->lock); } void del_timer(timer_node_t *node) { node->cancel = 1; } timer_t * timer_create_timer() { timer_t *r=(timer_t *)malloc(sizeof(timer_t)); memset(r,0,sizeof(*r)); int i,j; for (i=0;i<TIME_NEAR;i++) { link_clear(&r->near[i]); } for (i=0;i<4;i++) { for (j=0;j<TIME_LEVEL;j++) { link_clear(&r->t[i][j]); } } spinlock_init(&r->lock); r->current = 0; return r; } uint64_t gettime() { uint64_t t; #if !defined(__APPLE__) || defined(AVAILABLE_MAC_OS_X_VERSION_10_12_AND_LATER) struct timespec ti; clock_gettime(CLOCK_MONOTONIC, &ti); t = (uint64_t)ti.tv_sec * 100; t += ti.tv_nsec / 10000000; #else struct timeval tv; gettimeofday(&tv, NULL); t = (uint64_t)tv.tv_sec * 100; t += tv.tv_usec / 10000; #endif return t; } void expire_timer(void) { uint64_t cp = gettime(); if (cp != TI->current_point) { uint32_t diff = (uint32_t)(cp - TI->current_point); TI->current_point = cp; int i; for (i=0; i<diff; i++) { timer_update(TI); } } } void init_timer(void) { TI = timer_create_timer(); TI->current_point = gettime(); }