金庆的专栏

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  423 随笔 :: 0 文章 :: 454 评论 :: 0 Trackbacks
自动删除的定时器队列

(金庆的专栏)

网游服务器中使用定时器队列代替角色遍历可以提高性能,同时简代逻辑。
游戏主循环中不必遍历角色,只需触发定时器动作。

主循环代码如:

RootTimerQueue & rTimerQueue = Singleton<RootTimerQueue>();
do
{
    UpdateTimeNow();  // update m_rNow
    bool bTimerActed = rTimerQueue.TryToTickOne(m_rNow);
    size_t nCount = HandleMessages();
    
    if (0 == nCount)
    {
        if (m_bStopped) break;
        if (bTimerActed) continue;
        boost::this_thread::sleep(boost::posix_time::milliseconds(1));
    }
} while (true);

定时器运行与主循环线程。
RootTimerQueue为根队列,单件,各功能的定时器为RootTimerQueue的子队列。

定时器队列需要一个自动删除功能。
如角色相关的一大堆定时器动作需要在角色退出时自动删除。
不然定时器动作在角色不存在时或重新登录后触发,会造成错误。
如果采用角色临时ID(会话ID), 每次登录都不相同,
可以避免错误,定时器动作就成为一个空动作。
但是该动作一直存在于队列中会占用内存,无法清除。

解决方案是每个角色拥有一个自己的定时器队列,该队列为根队列的子队列。
角色退出时,子队列的删除会自动删除所有的定时器动作。

class TimerQueue : boost::noncopyable
{
public:
    typedef boost::function<void ()> Action;
    typedef UInt64 TimerId;
    
public:
    TimerId Insert(time_t tStart, const Action & act, unsigned int nIntervalSec = 0);    
    void Erase(TiemrId id);
    
public:
    void SetParent(TimerQueue * p);

public:
    bool TryToTickOne(time_t tNow);
};

时间精度为秒,因为现在的应用只需秒即可,可以稍加改动替换成高精度时间。

定时器动作分为2类:
1. 只做一次,nIntervalSec为0表示只做一次。
2. 重复做,nIntervalSec非0.

添加定时器只有一个开始时间(有需要可以加个结束时间)。
开始时间可以是登录前的一个时间点,这样就可以计算离线期间所发生的改变。

通过SetParent()将自己设为某个队列的子队列。

实现部分如下:

class TimerQueue : boost::nonecopyable
{
private:
    void TickOne();
    void SetNextTime();
    void EraseParentTimer();
    
private:
    typedef UInt64 TimerSeq;
    
private:
    struct TimerItem
    {
        time_t t;
        TimerSeq seq;
        Action act;
        unsigned int nIntervalSec;
    };
    
private:
    void InsertItem(const TimerItem & itm);
    TimerItem Pop();
    
private:
    struct TimerOrder {};
    
    typedef boost::multi_index::multi_index_container
    <
        TimerItem,
        
        boost::multi_index::indexed_by
        <
            boost::multi_index::ordered_unique<
                boost::multi_index::tag<TimerOrder>,
                boost::multi_index::composite_key<
                    TimerItem,
                    boost::multi_index::member<TimerItem, time_t, &TimerItem::t>,
                    boost::multi_index::member<IimerItem, TimerSeq, &TimerItem::seq>
                >  // composite_key
            >,  // ordered_unique
            boost::multi_index::hashed_unique<
                boost::multi_index::tag<TimerId>,
                boost::multi_index::member<TimerItem, TimerSeq, &TimerItem::seq>
            >  // hashed_unique
        >  // indexed_by
    > TimerItemSet;
    
    typedef TimerItemSet::index<TimerOrder>::type QueueByTime;
    typedef TimerItemSet::index<TimerId>::type QueueById;
    
private:
    TimerItemSet m_set;

private:
    TimerSeq m_seq;
    time_t m_tNext;
    
    TimerQueue * m_pParent;
    TimerId m_idParentTimer;  // Used to erase timer from parent
};


TimerItemSet以时间和序号排序,允许时间相同,但序号是唯一的。
并且也能用ID号索引,用于删除定时器。

TimerQueue.cpp

TimerQueue::TimerQueue()
    : m_seq(0)  // 0 is illegal
    , m_pParent(NULL)
    , m_idParentTimer(0)
{
    SetNextTime();
}

TimerQueue::~TimerQueue()
{
    EraseParentTimer();
}

TimerQueue::TimerId TimerQueue::Insert(
    time_t tStart, const Action & act, unsigned int nIntervalSec)
{
    TimerItem itm = {tStart, ++m_seq, act, nIntervalSec};
    InsertItem(itm);
    return itm.seq;
}

TimerQueue::Erase(TimerId id)
{
    SetById & rSet = mset.get<TimerId>();
    SetById::iterator itr = rSet.find(id);
    if (itr == rSet.end())
        return;
        
    time_t t = (*itr).t;
    TimerSeq seqErase = (*itr).seq;  // Used to set next time.
    TimerSeq seqFront = m_set.get<TimerOrder>().begin()->seq;
    rSet.erase(itr);
    if (seqErase == seqFront)
        SetNextTime();
}

void TimerQueue::SetParent(TimerQueue * p)
{
    EraseParentTimer()
    m_pParent = p
    SetNextTime();
}

bool TimerQueue::TryToTickOne(time_t tNow)
{
    if (tNow < m_tNext)
        return false;
    if (m_set.empty())
        return false;
        
    TickOne();
    return true;
}

void TimerQueue::TickOne()
{
    BOOST_ASSERT(!m_set.empty());
    TimerItem itm = Pop();
    itm.act();
    
    if (0 == itm.nIntervalSec)
        return;
    itm.t += itm.nIntervalSec;
    InsertItem(itm);
}

TimerQueue::TimerItem TimerQueue::Pop()
{
    BOOST_ASSERT(!m_set.empty());
    QueueByTime & rQ = m_set.get<TimerOrder>();
    QueueByTime::iterator itr = rQ.begin();
    TimerItem itm = *itr;
    rQ.erase(itr);
    BOOST_ASSERT(itm.t == m_tNext);
    SetNextTime();
    return itm;
}

void TimerQueue::SetNextTime()
{
    EraseParentTimer();
    
    if (m_set.empty())
    {
        m_tNext = std::numeric_limits<time_t>::max();
        return;
    }
    time_t tNew = (*m_set.get<TimeOrder>().begin()).t;
    m_tNext = tNew;
    if (NULL == m_pParent)
        return;
    m_idParentTimer = m_pParent->Insert(m_tNext,
        boost::bind(&TimerQueue::TickOne, this));
    BOOST_ASSERT(m_idParentTimer);  // 0 is illegal ID.
}

void TimerQueue::EraseParentTimer()
{
    if (m_pParent && m_idParentTimer)
        m_pParent->Erase(m_idParentTimer);
    m_idParentTimer = 0;
}

void TimerQueue::InsertItem(const TimerIten & itm)
{
    m_set.insert(itm);
    if (itm.t < m_tNext)
        SetNextTiem();
}


RootTimerQueue是TimerQueue的子类。
另外再派生子类
class LogicTimerQueue : public TimerQueue
{
public:
    LogicTimerQueue();
};

LogicTimerQueue::LogicTimerQueue()
{
    SetParent(&Singleton<RootTimerQueue>());
}

这样就让每个LogicTimerQueue成为根队列的子队列。

玩家类中就可以添加LogicTimerQueue成员,来执行定时动作。

class Player
{
public:
    Player()
    {
        ...
        // Check mail every hour.
        m_timer->Insert(time(NULL),
            boost::bind(&Player::CheckMail, this),
            3600);
        ...
    }
    
...
    
private:
    LogicTimerQueue m_timer;
    ...
};
posted on 2012-06-20 13:28 金庆 阅读(1138) 评论(2)  编辑 收藏 引用 所属分类: 1. C/C++2. 网游开发

评论

# re: 自动删除的定时器队列 2012-11-14 09:08 zozoiiiiii
class Player
{
Player(){注册定时器}
~Player(){注销定时器}
}

利用构造函数和析构函数来保证角色退出时定时器的注册和注销,不很简单么,干嘛要那么复杂  回复  更多评论
  

# re: 自动删除的定时器队列 2012-11-14 19:23 金庆
@zozoiiiiii
没错,是在~Player()中注销的。上面的代码是如何注销。假设某个Player注册了上千个定时器,需要在~Player()中调用上千次注销方法吗?  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理