时间轮是Libco中管理超时事件的定时器,在这之前先介绍一下定时器。
常用的定时器数据结构有:
- 链表:redis中的定时器是同链表实现的,时间复杂度为On,性能很差,但是redis中的超时时间只有一个所以被退化为指针。
- 堆:堆顶是最近的超时事件,时间复杂度为logn,性能比链表较好,但是不能支持随机删除。
- 红黑树:最左边的节点为最小超时事件,时间复杂度为logn。
- 时间轮:时间轮本质是由数组实现,每个下标代表一个时间刻度,通过
指针的移动代表时间流逝,因为是用数组,所以时间复杂度为O1,Linux内核也通过时间轮来作为定时器,但是时间轮的超时时间是有限制的,取决与数组的大小,Linux中采用了多级时间轮,一种类似生活中水表的结构来记录更久的超时时间。
接下来介绍Libco中时间轮的数据结构:
struct stTimeout_t
{
stTimeoutItemLink_t *pItems;
int iItemSize;
unsigned long long ullStart;
long long llStartIdx;
};
这是时间轮中轮的结构,pItems是一个数组,大小为iItemSize,默认为60*1000,一个下标代表一毫秒,所以时间轮大小为60秒,数组通过取模的方式来实现循环。ullStart是目前的超时管理器最早的时间,llStartIdx是目前最早的时间所对应的pItems上的索引。
每个下标都是一个stTimeoutItemLink_t 结构:
struct stTimeoutItemLink_t
{
stTimeoutItem_t *head;
stTimeoutItem_t *tail;
};
指向一个链表的头节点和尾节点。
这是节点的数据结构:
struct stTimeoutItem_t
{
enum
{
eMaxTimeout = 40 * 1000 //40s
};
stTimeoutItem_t *pPrev; // 前一个元素
stTimeoutItem_t *pNext; // 后一个元素
stTimeoutItemLink_t *pLink; // 该链表项所属的链表
unsigned long long ullExpireTime;
OnPreparePfn_t pfnPrepare; // 预处理函数,在eventloop中会被调用
OnProcessPfn_t pfnProcess; // 处理函数 在eventloop中会被调用
void *pArg; // self routine pArg 是pfnPrepare和pfnProcess的参数
bool bTimeout; // 是否已经超时
};
时间轮O1的时间复杂度一是通过数组下标来实现O1的索引,由于超时节点是以链表方式存储,所以他的插入也为O1,如果要取出节点,因为下标所在的节点都是同一超时时间,所以只需要将整个链表接到处理事件的链表即可,这个时间复杂度也为O1。
插入超时节点的函数:
int AddTimeout( stTimeout_t *apTimeout,stTimeoutItem_t *apItem ,unsigned long long allNow )
{
// 当前时间管理器的最早超时时间
if( apTimeout->ullStart == 0 )
{
// 设置时间轮的最早时间是当前时间
apTimeout->ullStart = allNow;
// 设置最早时间对应的index 为 0
apTimeout->llStartIdx = 0;
}
if( allNow < apTimeout->ullStart )
{
co_log_err("CO_ERR: AddTimeout line %d allNow %llu apTimeout->ullStart %llu",
__LINE__,allNow,apTimeout->ullStart);
return __LINE__;
}
if( apItem->ullExpireTime < allNow )
{
co_log_err("CO_ERR: AddTimeout line %d apItem->ullExpireTime %llu allNow %llu apTimeout->ullStart %llu",
__LINE__,apItem->ullExpireTime,allNow,apTimeout->ullStart);
return __LINE__;
}
// 计算当前事件的超时时间和超时管理器的最早时间的差距
int diff = apItem->ullExpireTime - apTimeout->ullStart;
if( diff >= apTimeout->iItemSize )
{
co_log_err("CO_ERR: AddTimeout line %d diff %d",
__LINE__,diff);
return __LINE__;
}
/*
计算出该事件的超时事件在超时管理器所在的槽的位置
apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );
然后在该位置的槽对应的超时链表的尾部添加一个事件
*/
AddTail( apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );
return 0;
}
这是每次epoll返回后取出所有超时事件的函数:
inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
{
if( apTimeout->ullStart == 0 )
{
apTimeout->ullStart = allNow;
apTimeout->llStartIdx = 0;
}
// 如果当前时间还未达到最早的超时时间,则直接返回
if( allNow < apTimeout->ullStart )
{
return ;
}
// 用当前时间减去最早超时时间,因为每一项代表1ms
// 所以cnt刚好就代表了,超时的个数
int cnt = allNow - apTimeout->ullStart + 1;
if( cnt > apTimeout->iItemSize )
{
cnt = apTimeout->iItemSize;
}
if( cnt < 0 )
{
return;
}
for( int i = 0;i<cnt;i++)
{
int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;
// 把该格子上的所有超时时间都放进去(同一时刻可能有多个超时时间)
Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx );
}
apTimeout->ullStart = allNow;
apTimeout->llStartIdx += cnt - 1;
}
这就是时间轮大部分的实现了,其中的链表操作都通过模板来进行泛化,就不具体讲解了。