定时器实现与分析
描述
服务器中,定时器是一个很重要的组件。最近在看自己项目中的定时器实现,参考了一些资料,也思考了一下几个可以优化的方向。为了简化下面对比的几个实现,首先抽象出定时器应该有的几个操作:
注册定时器
void schedule(const IEventPtr& handler, const DateTime beginDt, const Interval& interval);
停止定时器
void cancel(const IEventPtr& handler);
触发定时器
void expireTimer();
其中注册的定时器分两种,一种只执行一次,注册时interval为0;另一种为可重复执行的定时器,interval为第一次启动后的执行间隔。
最原始直观的做法——链表实现
链表实现的定时器是最简单直观的做法,每一个定时器作为一个链表的节点,添加的时候就直接插入到链表末端,时间复杂度O(1),然而停止定时器的时候就需要遍历链表,最坏情况需要遍历整个链表,时间复杂度为O(n),而触发定时器同样需要用当前时间与所有定时器作比较,触发比当前时间小的定时器,时间复杂度也为O(n)。
以下为简化后的几个基础操作:
定时器节点:
class TimeNode
{
public:
TimeNode(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
: _handler(handler), _time(beginDt), _interval(interval) {}
DateTime _time; //触发时间
Interval _interval; //触发间隔
IEventPtr _handler; //定时器指针
};
typedef std::list<TimeNode*> NodeList;
定时器管理类:
class TimeMgr
{
public:
void schedule(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
{
TimeNode* node = new TimeNode(handler, begindDt, interval);
_list.push_back(node);
}
void cancel(const IEventPtr& handler)
{
for (auto it = _list.begin(); it != _list.end(); ++it)
{
if ((*it)->_handler == handler)
{
auto node = *it;
_list.erase(it);
delete node;
break;
}
}
}
void expireTimer()
{
DateTime now;
for (auto it = _list.begin(); it != _list.end();)
{
if ((*it)->_time <= now)
{
(*it)->_handler->execute();
if ((*it)->_interval > 0)
{
(*it)->_time = now + (*it)->_interval;
++it;
}
else
{
auto node = *it;
_list.erase(it++);
delete node;
}
}
else
{
++it;
}
}
}
private:
NodeList _list;
};
优化做法I——排序链表实现
通过上面的分析可以发现,虽然使用链表很直观简单,但是效率堪忧,无论查找删除都是十分耗时的操作,也可以看出来有很多地方可以优化。考虑把链表做成按时间排序的形式,同时用一个map来保存节点指针,那么除了插入操作会变为O(n),删除操作和触发定时器的轮询都可以变为O(1),每次只需要轮询链表头部即可。
定时器节点:
class TimeNode
{
public:
TimeNode(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
: _handler(handler), _time(beginDt), _interval(interval)
{
_id = assignId();
}
DateTime _time; //触发时间
Interval _interval; //触发间隔
IEventPtr _handler; //定时器指针
TimeNode* _prev; //上一节点
TimeNode* _next; //下一节点
int _id;
};
定时器管理类:
class TimeMgr
{
public:
int schedule(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
{
TimeNode* node = new TimeNode(handler, begindDt, interval);
addNode(node);
return node->_id;
}
void cancel(int id)
{
eraseNode(id);
}
void expireTimer()
{
DateTime now;
while (_listHead && _listHead->_time <= now)
{
auto node = _listHead;
_listHead = _listHead->_next;
node->_handler->execute();
if (node->_interval > 0)
{
node->_time = now + (*it)->_interval;
addNode(node);
}
else
{
delete node;
}
}
}
private:
void addNode(TimeNode* node);
void eraseNode(int id);
TimeNode* _listHead;
std::map<int, TimeNode*> _nodeMap;
};
优化做法II——最小堆实现
经过上面将链表改为排序链表并额外存储了节点指针后,插入操作复杂度变为O(n),删除操作和触发定时器的复杂度变为O(1)。但同时考虑到插入操作的O(n)还是有点慢,结合平时使用的数据结构和实际场景中删除定时器操作不多的特点,考虑使用最小堆来处理。此时,插入和删除操作复杂度将会变为O(lgn),而触发定时器的复杂度仍然为O(1)。
定时器节点:
class TimeNode
{
public:
TimeNode(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
: _handler(handler), _time(beginDt), _interval(interval) {}
DateTime _time; //触发时间
Interval _interval; //触发间隔
IEventPtr _handler; //定时器指针
};
struct cmp
{
bool operator () (TimeNode* node1, TimeNode* node2)
{
return node1->_time > node2->_time;
}
}
typedef std::priority_queue<TimeNode*, std::vector<TimeNode*>, cmp> TimeQueue;
定时器管理类:
class TimeMgr
{
public:
void schedule(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
{
TimeNode* node = new TimeNode(handler, begindDt, interval);
_queue.push(node);
}
void expireTimer()
{
DateTime now;
while (!_queue.empty() && _queue.top()->_time <= now)
{
auto node = _queue.top();
_queue.pop();
node->_handler->execute();
if (node->_interval > 0)
{
node->_time = now + (*it)->_interval;
_queue.push(node);
}
else
{
delete node;
}
}
}
private:
TimeQueue _queue;
};
优化做法III——红黑树(map)实现
上面用最小堆实现后的算法复杂度已经降为O(lgn),但是由于标准库中的make_heap()等函数不能高效删除heap中的元素,所以如果需要提高性能还是需要让Timer自己记录自己在堆中的位置比较好。
同样,如果使用红黑树(标准库中的set/map)来实现,时间复杂度也一样为O(lgn)。正常来说,map的内存使用应该比heap要差一点,性能也要差一点,但是删除操作上的速度可以稍微弥补这一块的损失。选用map的原因是作为游戏服务器,时长短、相同时间点触发的定时器很多,用时间作为key排序,用一个vector作为value,每个时间点触发后需要删除的操作只为一次,复杂度为O(lgn),而如果用堆,删除次数还是会相对较多。
定时器节点:
class TimeNode
{
public:
TimeNode(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
: _handler(handler), _time(beginDt), _interval(interval) {}
DateTime _time; //触发时间
Interval _interval; //触发间隔
IEventPtr _handler; //定时器指针
};
typedef std::vector<TimeNode*> TimeVec;
typedef std::map<DateTime, TimeVec*> TimeMap;
定时器管理类:
class TimeMgr
{
public:
void schedule(const IEventPtr& handler, const DateTime beginDt, const Interval& interval)
{
TimeNode* node = new TimeNode(handler, begindDt, interval);
auto it = _map.find(node->_time);
if (it == _map.end())
{
TimeVec* tv = new TimeVec();
tv.push_back(node);
_map[node->_time] = tv;
}
else
{
it->second->push_back(node);
}
}
void expireTimer()
{
DateTime now;
while (!_map.empty() && _map.begin()->first <= now)
{
auto nodes = _map.begin()->second;
_map.erase(_map.begin());
for (auto nodeIt = nodes.begin(); nodeIt != node.end(); ++nodeIt)
{
auto node = *nodeIt;
node->_handler->execute();
if (node->_interval > 0)
{
node->_time = now + (*it)->_interval;
auto it = _map.find(node->_time);
if (it == _map.end())
{
TimeVec* tv = new TimeVec();
tv.push_back(node);
_map[node->_time] = tv;
}
else
{
it->second->push_back(node);
}
}
else
{
delete node;
}
}
}
}
private:
TimeMap _map;
};