定时器主要用于需要使⽤超时机制的功能。定时器的实现有两种方式:⼀种是,⽹络事件和时间事件在⼀个线程当中配合使⽤;例如nginx、redis;第⼆种是,⽹络事件和时间事件在不同线程当中处理;例如skynet。
定时器的设计
接口设计
定时器主要是用在定时任务中,所以主要需要的是添加、删除定时任务,找到最近要发⽣的定时任务和更新检测定时器,这几个接口,当然对于定时器本身,还有初始化、清除定时器资源等接口。
数据结构选择
由于定时器中存在的是一个个定时任务,每个任务都需要有一个时间戳存放任务的过期时间,所以很容易就会想到要使用有序的数据结构,而且增加删除操作不能影响该结构有序。由于定时器中用得最多的操作是找到最近要发⽣的定时任务,所以这里会把这个操作的时间复杂度单独拿出来讨论。
1.红黑树
是一种平衡二叉搜索树,对于增删查,时间复杂度为O(log n);由于最⼩节点为最左侧节点,所以找到最近要发⽣的定时任务的时间复杂度为O(log n)。nginx的定时器使用的是红黑树。
int init_timer()
{
ngx_rbtree_init(&timer, &sentinel, ngx_rbtree_insert_timer_value);
return 0;
}
void add_timer(timer_entry_t *te, uint32_t msec)
{
msec += current_time();
printf("add_timer expire at msec = %u\n", msec);
te->timer.key = msec;
ngx_rbtree_insert(&timer, &te->timer);
}
void del_timer(timer_entry_t *te)
{
ngx_rbtree_delete(&timer, &te->timer);
}
void expire_timer()
{
timer_entry_t *te;
ngx_rbtree_node_t *sentinel, *root, *node;
sentinel = timer.sentinel;
uint32_t now = current_time();
for (;;)
{
root = timer.root;
if (root == sentinel) break;
node = ngx_rbtree_min(root, sentinel);
if (node->key > now) break;
printf("touch timer expire time=%u, now = %u\n", node->key, now);
te = (timer_entry_t *) ((char *) node - offsetof(timer_entry_t, timer));
te->handler(te);
ngx_rbtree_delete(&timer, &te->timer);
free(te);
}
}
这里基于nginx的红黑树,实现定时器的几个接口。这里再提示一下,红黑树的实现还需要注意是否允许key值重复。
2.最小堆
根节点的key值总是该结构中最小的,对于增查,时间复杂度为O(log n);对于删,时间复杂度为O(n),因为最小堆中数据并不有序,所以需要遍历查找,但是可以通过辅助数据结
构(hashtable来快速索引节点)来加快删除操作;由于最⼩节点为根节点,所以找到最近要发⽣的定时任务的时间复杂度为O(1)。最小堆是使用最广泛的定时器数据结构,左右子树高度差最大为1,插入、删除也比红黑树快。go语言使用最小堆作为定时器。
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::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;
}
}
3.跳表
加了索引指针的有序链表,以后再分析。redis中有时会使用。因为redis中定时任务较少,所以一般用的是无序单链表,如果定时任务多,会改用跳表。
4.时间轮
对于增删查,时间复杂度为O(1);查找最⼩节点也为O(1)。操作粒度小,但是删除操作很不方便,因为不知道待删除节点是在什么位置。时间轮可以用于多线程,但是锁的粒度还是整个定时器。linux内核的定时器使用时间轮。时间轮增加操作只从单个定时任务触发,忽略定时任务之间的⼤⼩关系;⽽红⿊树、最⼩堆、跳表的有序性依赖定时任务之间的⼤⼩关系。
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(s_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, int threadid)
{
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;
node->id = threadid;
if (time <= 0)
{
node->callback(node);
free(node);
spinlock_unlock(&TI->lock);
return NULL;
}
add_node(TI, node);
spinlock_unlock(&TI->lock);
return node;
}
void move_list(s_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(s_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(s_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(s_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;
}
s_timer_t * timer_create_timer()
{
s_timer_t *r=(s_timer_t *)malloc(sizeof(s_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();
}
void clear_timer()
{
int i,j;
for (i=0;i<TIME_NEAR;i++)
{
link_list_t * list = &TI->near[i];
timer_node_t* current = list->head.next;
while(current)
{
timer_node_t * temp = current;
current = current->next;
free(temp);
}
link_clear(&TI->near[i]);
}
for (i=0;i<4;i++)
{
for (j=0;j<TIME_LEVEL;j++)
{
link_list_t * list = &TI->t[i][j];
timer_node_t* current = list->head.next;
while (current)
{
timer_node_t * temp = current;
current = current->next;
free(temp);
}
link_clear(&TI->t[i][j]);
}
}
}
这里实现的是一个多层级的时间轮,skynet中的定时器就是这样实现的。这里再提示一下,为了避免频繁地创建和销毁内存,如果知道了元素所需最大个数,可以将该元素以数组的形式声明成全局静态变量。