对于游戏服务器来说 定时器必不可少,我们设计定时器的方法一般有两种:
一种是设计一个定时器队列。对定时器的超时时间进行排序,每次在服务器的帧循环中从头开始检查定时器是否超时,如果超时了,把定时器从队列中移除,然后执行定时器回调函数,如果没有超时直接跳出循环。最简单的是我们利用STL库的map容器,map容器的内部实现是红黑树,遍历的时候是从小到大遍历, 我们把定时器的下次执行时间作为键, 每次在帧循环里遍历map容器,如果键小于当前时间则超时了,我们就执行定时器内容,然后删除定时器继续往后遍历。 如果键大于当前时间则没有超时直接跳出遍历。优化的版本是我们设计一个普通的队列,每次检查最小的超时时间,如果超时了就执行定时器,继续检查直到没有超时。而每次我们检查时会找出最小的那个时间节点,所以我们用最小堆算法,每次只查找最小的。这样不需要在每次的插入删除时进行排序,但是每次检查时需要求出最小的那个时间节点。
另一种是在帧上面做的定时器(原理上是个时间轮),这里我根据游戏服务器的特点,自己做了特殊的定时器。超时时间跟帧绑定,比如:我们的逻辑帧时间间隔时30ms, 而我们将在2000ms后执行定时器操作, 则我们把超时时间转换成帧数 (2000 + (30 - 1)) / 30 = 67, 意思我们将在67帧以后超时,如果我们的当前帧时第200帧,则我们将在第267帧执行定时器操作。 由此我们定义一个键值映射的容器(可以用std::map,也可以用std::unordered_map), 键是帧数,而值定义为该帧的超时队列。服务器每帧的循环中对这个容器查找,是否有该帧的超时队列。如果有则执行对列中所有定时器,然后删除掉。而我们在定时器插入的时候只需计算出所在帧数数,进行插入操作就好了。与正式的时间轮相比,精确度没有那么高,比如说某一帧执行时间超时了,那么后面每帧的时间都会超时,但是这个基本不会影响游戏的逻辑执行,而如果每帧都超时则服务器的压力到达极限了,不应该是定时器的问题,而要从其他的方面查找问题了,这种方式大大的减少了定时器的运算,时间基本上的O(1), 理论上定时器不限个数可以无限加。
下面贴一下我的部分代码片段:
// 定义定时器,和定时器容器,定时器帧超时容器
// 定时器相关定义:
struct TimerInfo
{
TimerInfo() {}
long long id = 0; // id
int interval = 0; // 时间间隔
int maxCount = 0; // 最大运行次数
int curCount = 0; // 当前已经运行次数
ILogicService::TimerProcFunc func = NULL; // 回调函数
ILogicService* self = NULL; // 逻辑服指针
void* param = NULL; // 回调函数参数
LinkNode timeoutNode; // 超时节点
};
// 定时器存储容器
StoreHash m_hashTimer;
// 定时器超时容器
StoreHash m_hashTimerOut;
// 添加操作:
// 添加定时器
long long ServerCore::LogicServiceAddTimer(ILogicService* pSelf, int startTime,
int repeat, int maxRunCount, ILogicService::TimerProcFunc cb, void* param)
{
if (m_bClosing || pSelf->m_nStatus >= ServiceStatus_DoDestory)
{
return 0;
}
// 创建定时器信息
TimerInfo* pTimer = (TimerInfo*)je_malloc(sizeof(TimerInfo));
if (!pTimer)
{
return 0;
}
long long id = m_TimerID++;
new (pTimer) TimerInfo(id, repeat, maxRunCount, 0, cb, pSelf, param);
pTimer->timeoutNode.front = pTimer->timeoutNode.next = NULL;
m_hashTimer.Insert(id, pSelf->GetServiceID(), pTimer);
// 添加到超时列表
AddToTimeroutList(startTime, pTimer);
return id;
}
// 添加到超时队列:// m_lCoreHeartCount 为当前帧序号
// 添加到超时列表
void ServerCore::AddToTimeroutList(int startTime, TimerInfo* pTimer)
{
if (!pTimer)
{
return;
}
if (pTimer->timeoutNode.front)
{
LinkRemoveSelf(&pTimer->timeoutNode);
}
// 多少帧之后超时
int heartCount = (startTime + (m_nHeartInterval - 1)) / m_nHeartInterval;
if (heartCount == 0)
{
heartCount = 1;
}
// 超时心跳帧
long long TimeoutHeart = m_lCoreHeartCount + heartCount;
// 加入到超时心跳帧
auto iter = m_hashTimerOut.find(TimeoutHeart);
if (iter == m_hashTimerOut.end())
{
iter = m_hashTimerOut.Insert(TimeoutHeart, 0, LinkNode());
if (iter == m_hashTimerOut.end())
{
// WARNING....
return;
}
InitLink(&(iter->second));
}
LinkAddToTail(&(iter->second), &(pTimer->timeoutNode));
}
// 每帧遍历超时:// m_lCoreHeartCount 为当前帧序号
// 每帧超时定时器检查
void ServerCore::UpdateTimer()
{
// 当前帧是否有超时定时器
auto iter = m_hashTimerOut.find(m_lCoreHeartCount);
if (iter == m_hashTimerOut.end())
{
return;
}
// 遍历所有超时定时器
while (auto pNode = LinkFirst(&(iter->second)))
{
if (!LinkNodeValid(&(iter->second), pNode))
{
break;
}
TimerInfo* pTimer = LinkNodeData(&(iter->second), pNode, TimerInfo, timeoutNode);
// 从超时定时器中移除
LinkRemoveSelf(&(pTimer->timeoutNode));
// 执行回调
if (pTimer->func)
{
pTimer->func(pTimer->self, pTimer->param);
}
// 执行次数加1
++pTimer->curCount;
// 是否结束了
if ((pTimer->maxCount > 0 && pTimer->maxCount <= pTimer->curCount) ||
pTimer->interval <= 0)
{
// 结束,删除定时器
m_hashTimer.Remove(pTimer->id);
continue;
}
// 继续添加到下次执行的超时队列帧中
AddToTimeroutList(pTimer->interval, pTimer);
}
// 移除当前帧节点
m_hashTimerOut.erase(iter);
}