固定Tick时间策略
固定Tick时间:顾名思义就是指程序每次心跳的时间都是等长的、固定的。如图中的“图A”,Tick1和Tick2的时间是相等的,如果实际执行的比上次执行时间长(Run2 > Run1),则Sleep2 < Sleep1,同时满足等式:Tick1 = Tick2 = Run1 + Sleep1 = Run2 + Sleep2
Mangos-Zero
mangos-zero项目中的逻辑服务进程mangosd的心跳函数采用的方法,当更新的处理时间Run1大于固定大小的tick时间时,下一个tick到来时不sleep直接执行Run2,实现代码如下:
1: /// Heartbeat for the World
2: void WorldRunnable::run()
3: {
4: ///- Init new SQL thread for the world database
5: WorldDatabase.ThreadStart(); // let thread do safe mySQL requests (one connection call enough)
6: sWorld.InitResultQueue();
7:
8: uint32 realCurrTime = 0;
9: uint32 realPrevTime = WorldTimer::tick();
10:
11: uint32 prevSleepTime = 0; // used for balanced full tick time length near WORLD_SLEEP_CONST
12:
13: ///- While we have not World::m_stopEvent, update the world
14: while (!World::IsStopped())
15: {
16: ++World::m_worldLoopCounter;
17: realCurrTime = WorldTimer::getMSTime(); //----------------(1)
18:
19: uint32 diff = WorldTimer::tick(); //--------------(2)
20:
21: sWorld.Update( diff ); //--------------(3)
22: realPrevTime = realCurrTime;
23:
24: // diff (D0) include time of previous sleep (d0) + tick time (t0)
25: // we want that next d1 + t1 == WORLD_SLEEP_CONST
26: // we can't know next t1 and then can use (t0 + d1) == WORLD_SLEEP_CONST requirement
27: // d1 = WORLD_SLEEP_CONST - t0 = WORLD_SLEEP_CONST - (D0 - d0) = WORLD_SLEEP_CONST + d0 - D0
28: if (diff <= WORLD_SLEEP_CONST+prevSleepTime) //----------------(4)
29: {
30: prevSleepTime = WORLD_SLEEP_CONST+prevSleepTime-diff;
31: ACE_Based::Thread::Sleep(prevSleepTime);
32: }
33: else
34: prevSleepTime = 0;
35:
36: #ifdef WIN32
37: if (m_ServiceStatus == 0) World::StopNow(SHUTDOWN_EXIT_CODE);
38: while (m_ServiceStatus == 2) Sleep(1000);
39: #endif
40: }
41:
42: sWorld.KickAll(); // save and kick all players
43: sWorld.UpdateSessions( 1 ); // real players unload required UpdateSessions call
44:
45: // unload battleground templates before different singletons destroyed
46: sBattleGroundMgr.DeleteAllBattleGrounds();
47:
48: sWorldSocketMgr->StopNetwork();
49:
50: sMapMgr.UnloadAll(); // unload all grids (including locked in memory)
51:
52: ///- End the database thread
53: WorldDatabase.ThreadEnd(); // free mySQL thread resources
54: }
以上代码是游戏世界的主循环,看while循环里的代码,主要干下面几件事:
(1)从WorldTimer::getMSTime()得到一个uint32的值realCurrTime,realCurrTime是循环的(到增加到0xFFFFFFFF后,在增加就变成0),表示当前时间,单位是毫秒,是一个相对前一次tick的时间。
(2)使用WorldTimer::tick();计算上次tick到这次tick的时间差diff,该值理论上等于realCurrTime – realPrevTime
(3)sWorld.Update( diff );就是tick里的处理函数,游戏逻辑在这里得到更新处理。
(4)这里就是图所描述的,如果运行时间大于固定的tick时间,则不sleep继续占用CPU来处理更新,直到能在一个tick处理所有操作为止,这个时候才会sleep让出CPU时间片。
(5)WORLD_SLEEP_CONST就是固定的tick的时间长度,在这里是50ms
总结:现在可以回答本节前面的两个问题:在高负荷情况下mangos采用的方式提高服务器的响应速度,每个tick时间长度为50ms,也就是每秒钟更新20次,能满足更新的需求。
如果主逻辑循环是在调用epoll,那么可以利用epoll_wait的timeout参数进行代替sleep,实现如下:
uint64_t prevSleepTime = 0;
uint64_t curTime = 0;
uint64_t prevTime = x_time::now_msec();
while(!getTermnation())
{
curTime = x_time::now_msec();
// t = tick , s = sleep
//diff = s0 + t0 , diff 是两次循环的间隔时间
//希望一直保持 s1 + t1 = MAX_EPOLL_THREAD_TIMEOUT(100ms) , tick(t1)未知,但可以使用t0来作为参考值代替
//s1 = MAX_EPOLL_THREAD_TIMEOUT - t1 = MAX_EPOLL_THREAD_TIMEOUT - t0 = MAX_EPOLL_THREAD_TIMEOUT - (diff - s0)
uint64_t diff = 0;
if(prevTime > curTime)
{
diff = 0xFFFFFFFFFFFFFFFF - (prevTime - curTime);
}
else
{
diff = curTime - prevTime;
}
//如果运行时间大于固定的tick时间,则不sleep继续占用CPU来处理更新,直到能在一个tick处理所有操作为止,这个时候才会sleep让出CPU时间片
if(diff <= MAX_EPOLL_THREAD_TIMEOUT + prevSleepTime) // diff - s0 = t0 >= MAX_EPOLL_THREAD_TIMEOUT
{
prevSleepTime = MAX_EPOLL_THREAD_TIMEOUT - (diff - prevSleepTime);
}
else
{
prevSleepTime = 0;
}
prevTime = curTime;
this->check_queue(epfd);
int conns = epoll_wait(epfd,&events[0],MAX_EPOLL_EVENTS,prevSleepTime); //sleep
if(getTermnation()) return true;
for(int i=0;i<conns;i++)
{
int fd = events[i].data.fd;
x_tcp_task* task = (x_tcp_task*)events[i].data.ptr;
if(events[i].events & EPOLLIN)
{
task->ListenRecv();
}
else if(events[i].events & EPOLLOUT)
{
task->ListenSend();
}
}
this->update(diff);
}
return true;