zoukankan      html  css  js  c++  java
  • 定时器实现与分析

    定时器实现与分析

    描述

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

    注册定时器
    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;
    };
    
    作者:Herm
    本文版权归作者和博客园所有,欢迎转载,转载请标明出处(附上博客链接)。 如果您觉得本篇博文对您有所收获,请点击右下角的 [推荐],谢谢!
  • 相关阅读:
    ASP.NET 数据绑定常用代码及其性能分析
    替代Eval的两种方式
    C# MySQL 数据库操作类
    百度地图api经纬度气死我了!
    APP审核关于3.2.1金融资格的审核回复苹果
    ios 导航栏底部黑线隐藏
    ios 涉及到支付金额计算的相关总结
    ios 图片上传与压缩,UIImagePickerController设置中文
    ios UISegmentedControl的定制
    iOS APP应用之间的跳转
  • 原文地址:https://www.cnblogs.com/zhqherm/p/11805173.html
Copyright © 2011-2022 走看看