首頁  >  文章  >  系統教程  >  定時器組件在遊戲業務中的重要性及實現方式

定時器組件在遊戲業務中的重要性及實現方式

王林
王林原創
2024-07-18 11:17:12755瀏覽

定時器組件在遊戲業務中的重要性及實現方式

定時器,是一個比較常見的組件。單就服務端來說,框架層面須要利用定時器來做會話的逾時,應用層面須要利用定時器來處理一些跟時間有關的業務邏輯。對於遊戲這些大量需求定時器的業務,一個簡單且有效率的定時器組件是必不可少的。

定時器組件的實作可以分為兩部分:

第一部分比較簡單,實現方法多種多樣,但是基本上都是跟語言相關的,因而並不是本文重點。所謂具象成的概念似乎就是指使用者怎麼來用。

【文章福利】小編在群組文件上傳了一些個人認為比較好得學習書籍、影片資料,有須要的可以進群【977878001】領取! ! !額外附贈價值699的核心資料包(含影片教學、電子書、實戰項目及程式碼)

应用定时器程序-1_linux 应用定时器_应用定时器设计电子钟

核心資料直通車:Linux核心原始碼技術學習路線+影片教學程式碼資料

學習直通車(騰訊課堂免費報考):Linux核心原始碼/顯存調優/檔案系統/進程管理/裝置驅動/網路合約堆疊

第二部分其實比起第一部須要更多的程式碼量,而且實作方法很有限。

這些模型用處就是簡單,找個學過資料結構的結業生能夠寫下來,不容易有bug。 add的時間複雜度是n(lgn),timeout的時間複雜度也是n(lgn)。

然而,假定我們的業務系統假如面對的是這樣的需求:短期內註冊了大量短時間內就要timeout的timer。很其實,最小堆的實現就有點難堪了。

下邊步入正文,小說君就介紹下我們在應用層怎樣實現一個linux內核風格的定時器。語言以C#為例。

為了做效能對比,我們要先實作一個基於最小堆的定時器管理器,最小堆的插口如下linux 應用定時器,具體實作就不港了,雖然是最基礎的資料結構。

public class PriorityQueue : IEnumerable
{
public PriorityQueue(IComparer comparer);
public void Push(T v);
public T Pop();
public T Top();
}

public interface ITimeManager
{
ITimer AddTimer(uint afterTick, OnTimerTimeout callback, params object[] userData);
FixedTick();
}

public class TrivialTimeManager : ITimeManager
{
// ...
}

之後是linux核心風格定時器的管理器實作。首先有一個設計前提:

linux 应用定时器_应用定时器程序-1_应用定时器设计电子钟

我們須要用tick來定義整個系統的時間精度下限。例如說對遊戲來說,10ms以下的精準度不須要care,而我們可以把tick的寬度設定為10ms。也就是說先掛起來的WaitFor(8ms)和後掛起來的WaitFor(5ms),有可能是後者先timeout的。一個tick為10ms,這麼一個32bit的tick能抒發的時間細度就有將近500天嵌入式linux培訓,遠超過一個伺服器組不重啟的時間了。

虽然这些定时器实现,就是由于这个抉择,在面对之前提到的问题时,方才具有了更佳的性能表现。每次按照tick领到timeout数组,直接dispatch,领到这个数组的时间是一个常数,而最小堆方式领到这个数组须要的时间是m*lgn。

因为空间有限,我们不可能做到每位即将timeout的tick都有对应的数组。考虑到虽然80%以上的timer的时间都不会超过2.55s,我们只针对前256个tick做这些优化举措即可。

那怎么处理注册的256tick以后的timer?我们可以把时间还比较长的timer置于更粗细度的数组中,等到还剩下的tick数大于256以后再把她们取下来重新整理一下数组能够搞定。

假如我们保证每一次tick都严格的做到:

保证这两点,就须要每位tick都对所有数组做一次整理。这样就得不偿失了,所以这儿有个trade-off,就是我通过一个表针(index),来标记我当前处理的position,每过256tick是一个cycle,才进行一次整理。而整理的成本就通过分摊在256tick中,增加了实际上的单位时间成本。

概念比较具象,接出来贴一部份代码。

常量定义:

linux 应用定时器_应用定时器程序-1_应用定时器设计电子钟

public const int TimeNearShift = 8;
public const int TimeNearNum = 1 << TimeNearShift;// 256
public const int TimeNearMask = TimeNearNum - 1;// 0x000000ff
public const int TimeLevelShift = 6;
public const int TimeLevelNum = 1 << TimeLevelShift;// 64
public const int TimeLevelMask = TimeLevelNum - 1;// 00 00 00 (0011 1111)

基础数据结构:

using TimerNodes = LinkedList;
private readonly TimerNodes[TimeNearNum] nearTimerNodes;
private readonly TimerNodes[4][TimeLevelNum] levelTimerNodes;

相当于是256+4*64个timer数组。

tick有32位,每一个tick只会timeout掉expire与index相同的timer。

循环不变式保证near表具有这样几个性质:

level表有4个,分别对应9到14bit,15到20bit,21到26bit,27到32bit。

因为原理都类似,我这儿拿9到14bit的表来说下循环不变式:

有了数据结构和循环不变式,前面的代码也就容易理解了。主要列一下AddTimer的逻辑和Shift逻辑。

private void AddTimerNode(TimerNode node)
{
var expire = node.ExpireTick;
if (expire < index)
{
throw new Exception();
}
// expire 与 index 的高24bit相同
if ((expire | TimeNearMask) == (index | TimeNearMask))
{
nearTimerNodes[expire & TimeNearMask].AddLast(node);
}
else
{
var shift = TimeNearShift;
for (int i = 0; i < 4; i++)
{
// expire 与 index 的高bit相同
var lowerMask = (1 <> shift)&TimeLevelMask].AddLast(node);
break;
}
shift += TimeLevelShift;
}
}
}

private void TimerShift()
{
// TODO index回绕到0的情况暂时不考虑
index++;
var ct = index;// mask0 : 8bit
// mask1 : 14bit
// mask2 : 20bit
// mask3 : 26bit
// mask4 : 32bit
var partialIndex = ct & TimeNearMask;
if (partialIndex != 0)
{
return;
}
ct >>= TimeNearShift;
for (int i = 0; i >= TimeLevelShift;
continue;
}
ReAddAll(levelTimerNodes[i], partialIndex);
break;
}
}

以上代码用c/c++重画后尝尝鲜味更佳。

实现大约就是这种了,接出来我们测一下究竟linux内核风格定时器比最小堆实现的定时器快了多少。

建立的测试用例和测试方式:

static IEnumerable BuildTestCases(uint first, uint second)
{
var rand = new Random();
for (int i = 0; i < first; i++)
{
yield return new TestCase()
{
Tick = (uint)rand.Next(256),
};
}
for (int i = 0; i < 4; i++)
{
var begin = 1U << (8 + 6*i);
var end = 1U << (14 + 6*i);
for (int j = 0; j < rand.Next((int)second * (4 - i)); j++)
{
yield return new TestCase()
{
Tick = (uint)rand.Next((int)(begin+end)/2),
};
}
}
}

{
var maxTick = cases.Max(c => c.Tick);
var results = new HashSet();
foreach (var c in cases)
{
TestCase c1 = c;
mgr.AddTimer(c.Tick, (timer, data) =>
{
if (mgr.FixedTicks == c1.Tick)
results.Add((uint) data[0]);
}, c.Id);
}
var begin = DateTime.Now;
for (int i = 0; i < maxTick+1; i++)
{
mgr.FixedTick();
}
var end = DateTime.Now;
}

建立测试用例时的参数first指大于等于256tick的timer数目,second是指小于256tick的timer数目。

first固定为一千万的测试结果:

Turun naik nisbah pecutan tidak begitu ketara, dan jika kedua terus menurun, nisbah pecutan pemasa kernel Linux sebenarnya akan meningkat secara beransur-ansur disebabkan peningkatan dalam kekerapan anjakan.

Kedua ditetapkan pada 1000:

Sama seperti inferens daripada ujian pertama, lebih tinggi bahagian pemasa dalam 256 detik, lebih besar kelebihan berbanding pemasa timbunan minimum.

Kesimpulan akhir: Kelebihan pemasa kernel Linux berbanding pemasa timbunan minimum masih sangat ketara Dalam kebanyakan kes, prestasinya lebih daripada 2 kali ganda, dan sangat disyorkan untuk menggunakannya.

Kali ini kod diletakkan pada githubPemasa aplikasi Linux, dan kerana tiada cara untuk meletakkan pautan ke perisian linux dalam artikel akaun langganan, selagi bahagian belakang menghantar mesej "pemasa" kepada Novel Jun, pautan github akan dibalas secara manual. Projek ini bukan sahaja mengandungi kod pelaksanaan pemasa gaya Linux gred industri, tetapi juga satu set Coroutine gaya Unity3D berdasarkan pemasa ini yang dilaksanakan oleh Xiaoxiaojun.

--Rangkaian Bahasa Inggeris Teknologi Kernel-Tubuhkan sidang kemuncak pertukaran dan perkongsian teknologi inti yang paling berwibawa di wilayah itu

Alamat asal: Memahami pelaksanaan pemasa gaya kernel Linux - sistem pengendalian - Rangkaian Bahasa Inggeris Teknologi Kernel - mewujudkan sidang kemuncak pertukaran dan perkongsian teknologi kernel yang paling berwibawa di wilayah (hak cipta milik pengarang asal, pelanggaran dan pemadaman)

以上是定時器組件在遊戲業務中的重要性及實現方式的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn