Home> System Tutorial> LINUX> body text

The importance of timer components in the game business and how to implement them

王林
Release: 2024-07-18 11:17:12
Original
647 people have browsed it

The importance of timer components in the game business and how to implement them

Timer is a relatively common component. As far as the server is concerned, the framework level needs to use timers to time out sessions, and the application level needs to use timers to handle some time-related business logic. For businesses such as games that require a large number of timers, a simple and efficient timer component is essential.

The implementation of the timer component can be divided into two parts:

The first part is relatively simple, and there are various implementation methods, but they are basically related to language, so it is not the focus of this article. The so-called concrete concept seems to refer to how users use it.

[Article Benefits] The editor has uploaded some learning books and video materials that I think are better in the group file. If you need them, you can join the group [977878001] to get them! ! ! Comes with an additional kernel information package worth 699 (including video tutorials, e-books, practical projects and codes)

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

Kernel information direct train: Linux kernel source code technology learning route + video tutorial code information

Learning express train (free registration in Tencent Classroom): Linux kernel source code/video memory tuning/file system/process management/device driver/network contract stack

The second part actually requires more code than the first part, and the implementation methods are very limited.

The purpose of these models is that they are simple. Find a graduate who has studied data structures to write them down, and they will not be prone to bugs. The time complexity of add is n(lgn), and the time complexity of timeout is also n(lgn).

However, assume that our business system faces such a demand: registering a large number of timers in a short period of time that will time out in a short period of time. In fact, the implementation of the minimum heap is a bit embarrassing.

Coming into the text below, Xiaoxiao will introduce how we implement a Linux kernel-style timer in the application layer. The language is C# as an example.

In order to make a performance comparison, we must first implement a timer manager based on the minimum heap. The interface of the minimum heap is as followslinux application timer. The specific implementation is not available, although it is the most basic data structure.

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

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

public class TrivialTimeManager : ITimeManager { // ... }
Copy after login

After that is the manager implementation of the linux kernel style timer. First there is a design premise:

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

We need to use tick to define the lower limit of time accuracy of the entire system. For example, for games, accuracy below 10ms does not require care, so we can set the tick width to 10ms. In other words, the WaitFor(8ms) that hangs up first and the WaitFor(5ms) that hangs up later may be timed out first. A tick is 10ms. The time granularity that such a 32-bit tick can express is nearly 500 days of embedded linux training, which is far longer than the time of a server group without restarting.

虽然这些定时器实现,就是由于这个抉择,在面对之前提到的问题时,方才具有了更佳的性能表现。每次按照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)
Copy after login

基础数据结构:

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

相当于是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; } } }
Copy after login

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; } }
Copy after login

以上代码用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), }; } } }
Copy after login

{ 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; }
Copy after login

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

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

加速率の変動はそれほど大きくなく、秒が減少し続けると、実際にはシフト周波数の増加により Linux カーネル タイマーの加速率が徐々に増加します。

2 番目は 1000 に固定されます:

最初のテストの推論と同様に、256 ティック以内のタイマーの割合が高くなるほど、最小ヒープ タイマーよりも有利になります。

最終結論: 最小ヒープ タイマーと比較した Linux カーネル タイマーの利点は依然として非常に大きく、ほとんどの場合、パフォーマンスは 2 倍以上であるため、これを使用することを強くお勧めします。

今回コードはgithub

Linuxアプリケーションタイマーに置かれており、サブスクリプションアカウントの記事にLinuxソフトウェアへのリンクを置く方法がないため、バックエンドがNovel Junにメッセージ「タイマー」を送信する限り、 github リンクは手動で返信されます。このプロジェクトには、産業グレードの Linux スタイルのタイマー実装コードだけでなく、Xiaoxiaojun によって実装されたこのタイマーに基づく Unity3D スタイルのコルーチンのセットも含まれています。

--カーネルテクノロジー英語ネットワーク-州内で最も権威のあるカーネルテクノロジー交換および共有サミットを設立

元のアドレス: Linux カーネル スタイルのタイマー実装の理解 - オペレーティング システム - カーネル技術英語ネットワーク - 州内で最も権威のあるカーネル技術交換と共有サミットの確立 (著作権はオリジナルの作成者に属し、侵害と削除は禁止されています)

The above is the detailed content of The importance of timer components in the game business and how to implement them. For more information, please follow other related articles on the PHP Chinese website!

source:itcool.net
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!