定时器实现
定时器应用
- 心跳检测
- 技能冷却
- 武器冷却
- 倒计时
- 每日/每周/每月 数值重置
- 其它需要使⽤超时机制的功能
定时器实现类型
对于服务端来说,驱动服务端逻辑的事件主要有两个,⼀个是⽹络事件,另⼀个是时间事件;
在不同框架中,这两种事件有不同的实现⽅式;
类型一、网络事件和时间事件在⼀个线程当中配合使用,例如nginx、redis
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(); // 时间事件处理
}
类型二、网络事件和时间事件在不同线程当中处理,例如skynet;
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();
// 清除定时器
// void clear_timer();
要点
- 有序的结构,且增加删除操作不影响该结构有序;
- 能快速查找最⼩节点;
- 时间轮增加操作只从单个定时任务触发,忽略定时任务之间的⼤⼩关系;⽽红⿊树、最⼩堆、跳表的有序性依赖定时任务之间的⼤⼩关系;
数据结构选择
- 红黑树:对于增删查,时间复杂度为 O(log2n);对于红⿊树最⼩节点为最左侧节点,时间复杂度为 O(log2n)。
- 最小堆:对于增查,时间复杂度为 O(log2n);对于删时间复杂度为 O(n) ,但是可以通过辅助数据结构(map或者hashtable来快速索引节点)来加快删除操作;对于最⼩节点为根节点,查找时间复杂度为 O(1);
- 跳表:对于增删查,时间复杂度为 O(log2n);对于跳表最⼩节点为最左侧节点,时间复杂度为O(1);但是空间复杂度⽐较⾼,为O(1.5n) ;
- 时间轮:对于增删查,时间复杂度为 O(1);查找最⼩节点也为 O(1);
红黑树
红黑树又名二叉搜索树,通过中序遍历可以得到有序的序列。红黑树最左侧的叶子结点是最小节点。因此如果用红黑树作为定时器的数据结构,搜索最小结点就是搜索最左侧叶子节点,时间复杂度是O(log2n)。
最小堆
满⼆叉树:所有的层节点数都是该层所能容纳节点的最⼤数量(满⾜ 2^n; n >= 0);
完全⼆叉树:若⼆叉树的深度为 h ,除了 h 层外,其他层的节点数都是该层所能容纳节点的最⼤
数量(满⾜ 2^n; n >= 0),且 h 层都集中在最左侧;
最⼩堆:
- 是⼀颗完全⼆叉树;
- 某⼀个节点的值总是⼩于等于它的⼦节点的值;
- 堆中每个节点的⼦树都是最⼩堆;
增加操作:
- 为了满足完全二叉树定义,找到叶子结点最左侧空缺处插入。
- 尝试上升操作(尝试与上层的结点比较大小,确定是否交换结点位置,这是一个递归操作)。
删除操作:
- 找到目标结点(如果是直接在最小堆查找,时间复杂度是O(n),因为无法保证左右子树的大小关系,所以需要全部遍历。优化点在可以维护map[pos]=val,快速找到待删除结点位置)。
- 将目标结点与最下层最右侧的结点交换(原因是最小堆是完全二叉树,所以如果删掉一个结点,那么删除后该最小堆的最下层最右侧必然为空)。
- 尝试下沉操作(尝试与下层的结点比较大小,确定是否交换结点位置,这是一个递归操作)。
代码
//minheap.h
#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秒;
linux内核定时器就是用时间轮实现的。
单层级时间轮
背景
⼼跳检测:
客户端每 5 秒钟发送⼼跳包;服务端若 10 秒内没收到⼼跳数据,则清除连接;
实际在开发过程中,若收到除了⼼跳包的其他数据,⼼跳检测也算通过,在这⾥为了简化流程,只判断⼼跳包;
假设我们使⽤ map<int, conn*> 来存储所有连接数;每秒检测 map 结构,那么每秒需要遍历所有的连接,如果这个map结构包含⼏万条连接,那么我们做了很多⽆效检测;考虑极端情况,刚添加进来的连接,下⼀秒就需要去检测,实际上只需要10秒后检测就⾏了;那么我们考虑使⽤时间轮来检测;
使用时间轮检测:每秒检测hash表中的一个hash[key++%60]中的链表(代表这一时刻应触发的事件)。
注意:这个例⼦只是⽤来帮助理解时间轮,不代表实际解决⽅案;
设计
- 准备⼀个数组存储连接数据;那么数组⻓度设置为多少?
- 考虑⼀秒内添加了多条连接,那么可以参考 hash 结构处理冲突的⽅式,⽤链表链接起来;
- 回到 1 中的问题,如果想 2 中链表稀疏,将数组⻓度设置⼤⼀些;如果想紧凑些,则将数
组⻓度设置⼩些(但是必须⼤于10); - 假设我们设置数组⻓度为 11;那么检测指针的移动可描述为 ++point %11;
m%n = m-n * floor(m/n)
优化:将 n 替换为 2^k ,这⾥ 恰好⼤于 n;这样一来 m%(2^k) 可以转化为 m&(2^k-1) ;
所以我们可以选择 16(2^4 ),那么检测指针移动可优化为 ++point&(16-1) ; - 考虑到正常情况下 5 秒钟发送⼀次⼼跳包,10 秒才检测⼀次,如下图到索引为 10 的时候并
不能踢掉连接;所以需要每收到⼀次⼼跳包则 used++ ,每检测⼀次 used-- ;当检测到
used == 0 则踢掉连接;
多层级时间轮
参照时钟表盘的运转规律,可以将定时任务根据触发的紧急程度,分布到不同层级的时间轮中;
假设时间精度为 10ms ;在第 1 层级每 10ms 移动⼀格;每移动⼀格执⾏该格⼦当中所有的定时任务;
当第 1 层指针从 255 格开始移动,此时层级 2 移动⼀格;层级 2 移动⼀格的⾏为定义为,将该格当中的定时任务重新映射到层级 1 当中;同理,层级 2 当中从 63 格开始移动,层级 3 格⼦中的定时任务重新映射到层级 2 ; 以此类推层级 4 往层级 3 映射,层级 5 往层级 4 映射;
真正执行到期事件回调函数callbackfunc的是在层级一,其他层级在移动一格时会把当前格的链表映射到上一层级中。
如何重新映射?定时任务的过期时间对上⼀层级的⻓度取余分布在上⼀层级不同格⼦当中;
数据结构
typedef struct timer_node {
struct timer_node *next;
uint32_t expire;
handler_pt callback;
uint8_t cancel; //是否过期标志,如果为1,则不执行callback
uint8_t repeat; //是否重复执行,如果为1,执行完callback后,重新插入到s_timer_t中
int id; // 此时携带参数
}timer_node_t;
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;
}s_timer_t;
添加节点
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;
}
}
}