定时器实现与分析
描述
服务器中,定时器是一个很重要的组件。最近在看自己项目中的定时器实现,参考了一些资料,也思考了一下几个可以优化的方向。为了简化下面对比的几个实现,首先抽象出定时器应该有的几个操作:
注册定时器 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; };