定时器实现与分析

定时器实现与分析

描述

服务器中,定时器是一个很重要的组件。最近在看自己项目中的定时器实现,参考了一些资料,也思考了一下几个可以优化的方向。为了简化下面对比的几个实现,首先抽象出定时器应该有的几个操作:

注册定时器
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;
};
上一篇:56. Merge Intervals


下一篇:Leetcode解题-Insert Interval