【Unity】高性能时间轮询器

unity时间轮询器

unity中需要使用的轮询器的次数不多(不会达到万级别),所以一般不需要本文后面的”高性能时间轮询器”,只需要简单写一个就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Unity专用:高性能二叉堆定时器
/// ✔ 添加 O(logN)
/// ✔ 取最早任务 O(1)
/// ✔ Tick 稳定,不会卡顿
/// ✔ GC 极低
/// ✔ 支持重复任务
/// </summary>
public sealed class TimerHeap
{
private class TimerTask
{
public ulong Id;
public float ExpireTime;
public float Interval;
public int RepeatCount;
public Action<object?>? Callback;
public object? Data;

public bool Cancelled;
}

// ---------- 堆结构(最小堆:ExpireTime 最小的在顶部) ----------
private readonly List<TimerTask> heap = new List<TimerTask>(128);

// ---------- 对象池 ----------
private readonly Stack<TimerTask> pool = new Stack<TimerTask>(128);

private ulong nextId = 0;

public ulong Schedule(Action<object?> callback, float delay, float interval = 0, int repeatCount = 0, object? data = null)
{
if (callback == null) throw new ArgumentNullException(nameof(callback));

if (delay <= 0f) delay = 0.0001f;
if (repeatCount != 0 && interval <= 0) throw new ArgumentOutOfRangeException(nameof(interval));

float expire = Time.time + delay;

var task = AllocateTask();
task.Id = ++nextId;
task.Callback = callback;
task.Data = data;
task.Interval = interval;
task.RepeatCount = repeatCount;
task.ExpireTime = expire;
task.Cancelled = false;

Push(task);

return task.Id;
}

public bool Cancel(ulong id)
{
// 延迟删除:不从堆中立即移除
foreach (var t in heap)
{
if (t.Id == id && !t.Cancelled)
{
t.Cancelled = true;
return true;
}
}
return false;
}

/// <summary>
/// 主线程调用(在 Update 中)
/// </summary>
public void Tick()
{
float now = Time.time;

while (heap.Count > 0)
{
var t = heap[0];

// 未到时间 → break
if (t.ExpireTime > now) break;

PopTop(); // 从堆中移除该任务

if (!t.Cancelled)
{
// 执行回调
try
{
t.Callback?.Invoke(t.Data);
}
catch (Exception ex)
{
Debug.LogException(ex);
}

// 重复任务
if (t.RepeatCount != 0)
{
if (t.RepeatCount > 0) t.RepeatCount--;

t.ExpireTime = now + t.Interval;
Push(t);
continue;
}
}

// 任务结束 → 回收
ReleaseTask(t);
}
}

#region ----------- 堆操作 -----------
private void Push(TimerTask task)
{
heap.Add(task);
HeapifyUp(heap.Count - 1);
}

private void PopTop()
{
int last = heap.Count - 1;

heap[0] = heap[last];
heap.RemoveAt(last);

if (heap.Count > 0)
HeapifyDown(0);
}

private void HeapifyUp(int index)
{
var heapList = heap;

while (index > 0)
{
int parent = (index - 1) >> 1;

if (heapList[index].ExpireTime >= heapList[parent].ExpireTime)
break;

(heapList[index], heapList[parent]) = (heapList[parent], heapList[index]);
index = parent;
}
}

private void HeapifyDown(int index)
{
var heapList = heap;
int count = heapList.Count;

while (true)
{
int left = (index << 1) + 1;
if (left >= count) break;

int smallest = left;

int right = left + 1;
if (right < count && heapList[right].ExpireTime < heapList[left].ExpireTime)
{
smallest = right;
}

if (heapList[index].ExpireTime <= heapList[smallest].ExpireTime)
break;

(heapList[index], heapList[smallest]) = (heapList[smallest], heapList[index]);
index = smallest;
}
}
#endregion

#region ----------- 内存池 -----------
private TimerTask AllocateTask()
{
if (pool.Count > 0)
{
return pool.Pop();
}
return new TimerTask();
}

private void ReleaseTask(TimerTask task)
{
task.Id = 0;
task.Callback = null;
task.Data = null;

pool.Push(task);
}
#endregion
}

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37


public sealed class LevelManager : SingletonBehaviour<LevelManager>
{
private TimerHeap _timeHeap = new TimerHeap();

private void Update()
{
_timeHeap.Tick();
}

/// <summary>
/// 添加一个以秒为单位的定时任务。
/// </summary>
/// <param name="callback">回调函数</param>
/// <param name="delay">延迟执行的秒数</param>
/// <param name="interval">重复周期的秒数 (0表示不重复)</param>
/// <param name="repeatCount">重复次数 (小于0无限, 0不重复)</param>
/// <param name="data">回调数据</param>
/// <returns>任务ID,用于取消</returns>
public ulong Schedule(Action<object?> callback, float delay, float interval = 0, int repeatCount = 0, object? data = null)
{
return _timeHeap.Schedule(callback, delay, interval, repeatCount, data);
}

/// <summary>
/// 取消一个定时任务。
/// </summary>
/// <param name="id">要取消的任务ID</param>
/// <returns>是否成功取消</returns>
public bool Cancel(ulong id)
{
if (_timeHeap == null) return false;
return _timeHeap.Cancel(id);
}
}

高性能时间轮询器

文章最后方有源码,源码的设计是通过Tick来计时的。

通过实际效果来说,就是如果将TimeWheel.SharedInstance.Tick();直接写在Unity的Update中,那么TimeWheel.SharedInstance.Schedule()的参数的单位则是Update的执行次数,而不是秒。

提高易用性

以下为我在unity中正在使用的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;
using Default;
using Modules.GameManager;
using Sirenix.OdinInspector;
using UnityEngine;

public class TimeWheelDriver : MonoBehaviour
{
public float TimeScale;
private TimeWheel _timeWheel;
private double _accumulatedTimeMs = 0f;

private void Awake()
{
_timeWheel = new TimeWheel(shouldSortBeforeExecution: false);
_timeWheel.OnCallbackError += HandleCallbackError;
}

private void Update()
{
// 1. 获取自上一帧以来经过的时间(秒),并转换为毫秒
_accumulatedTimeMs += Time.deltaTime * 1000f * TimeScale;

// 2. 根据累积的毫秒数,驱动时间轮 Tick 相应次数
// 例如,如果一帧过了16.67ms,那么就 Tick 16 次,并将剩余的 0.67ms 留到下一帧
while (_accumulatedTimeMs >= 1f)
{
_timeWheel.Tick();
_accumulatedTimeMs -= 1f;
}
}

private void HandleCallbackError(Exception ex)
{
// 在Unity控制台打印错误,而不是抛出导致程序中断
Log.Error($"{Time.fixedTimeAsDouble} TimeWheel callback error: {ex}");
}
}

原作者的取消事件有问题,在取消时间时未将事件类的Next和Prev置空,现已修改,详情可看private static void RemoveTask(ref TimerTask? head, TimerTask task)方法的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;

/// <summary>
/// Linux内核风格的分层时间轮
/// 特性:
/// 1. 五级时间轮分层
/// 2. 自动任务降级机制
/// 3. 无锁设计(基于CAS操作)
/// 4. 内存池优化
/// </summary>
public sealed class TimeWheel : IDisposable
{
#region Constants and Structs

private const int TVN_BITS = 6;
private const int TVR_BITS = 8;
private const int TVN_SIZE = 1 << TVN_BITS;
private const int TVR_SIZE = 1 << TVR_BITS;
private const int TVN_MASK = TVN_SIZE - 1;
private const int TVR_MASK = TVR_SIZE - 1;

public const long MAX_DELAY = (long)TVR_SIZE * TVN_SIZE * TVN_SIZE * TVN_SIZE * TVN_SIZE - 1;

private class TimerTask
{
public ulong Id;
public long ExecuteOrder;
public Action<object?>? Callback;
public object? Data;
public ulong ExpireTicks;
public ulong Interval;
public int RepeatCount;
public TimerTask? Next;
public TimerTask? Prev;

/// <summary>
/// 0=未激活, 1=已激活, 2=已取消
/// </summary>
public int State;
}

#endregion

#region Core Fields

private readonly TimerTask?[] tv1 = new TimerTask[TVR_SIZE];
private readonly TimerTask?[] tv2 = new TimerTask[TVN_SIZE];
private readonly TimerTask?[] tv3 = new TimerTask[TVN_SIZE];
private readonly TimerTask?[] tv4 = new TimerTask[TVN_SIZE];
private readonly TimerTask?[] tv5 = new TimerTask[TVN_SIZE];

private ulong nextId;
private long executeOrder;
private ulong currentTick;
private readonly bool shouldSortBeforeExecution;
private readonly object syncRoot = new();
private readonly Stack<TimerTask?> taskPool = new(1024);
private readonly List<TimerTask> taskListCache = new(1024);

#endregion

#region Public Interface

public static TimeWheel SharedInstance { get; private set; } = null!;

public static TimeWheel CreateSharedInstance(bool shouldSortBeforeExecution = true)
{
if (SharedInstance != null)
{
throw new Exception("SharedInstance already created");
}

return SharedInstance = new(shouldSortBeforeExecution);
}

public TimeWheel(bool shouldSortBeforeExecution = true)
{
this.shouldSortBeforeExecution = shouldSortBeforeExecution;
}

public event Action<Exception>? OnCallbackError;

/// <summary>
/// 添加定时任务
/// </summary>
/// <param name="callback">回调函数</param>
/// <param name="delay">延迟, 最小值1, 如果为0则自动设置为1</param>
/// <param name="interval">重复周期</param>
/// <param name="repeatCount">到期后, 再次重复次数(小于0无限, 0不重复)</param>
/// <param name="data">回调函数的数据————不要在callback中直接使用参数,以免发生闭包问题。将参数放在这里可以避免闭包问题</param>
/// <returns>任务句柄</returns>
public ulong Schedule(Action<object?> callback, ulong delay, ulong interval = 0, int repeatCount = 0, object? data = null)
{
if (callback == null) throw new ArgumentNullException(nameof(callback));
delay = delay switch
{
> MAX_DELAY => throw new ArgumentOutOfRangeException(nameof(delay), $"Delay must be less than {MAX_DELAY}, but got {delay}"),
0 => 1,
_ => delay
};

if (repeatCount != 0 && interval <= 0) throw new ArgumentOutOfRangeException(nameof(interval), $"Interval must be greater than zero");

var task = this.AllocateTask();
task.Id = ++this.nextId;
task.ExecuteOrder = Interlocked.Increment(ref this.executeOrder);
task.Callback = callback;
task.Data = data;
task.Interval = interval;
task.RepeatCount = repeatCount;
task.ExpireTicks = this.currentTick + delay;

lock (this.syncRoot)
{
this.InternalAddTask(task);
}

return task.Id;
}

/// <summary>
/// 取消定时任务
/// </summary>
public bool Cancel(ulong id)
{
lock (this.syncRoot)
{
foreach (var vec in new[] { this.tv1, this.tv2, this.tv3, this.tv4, this.tv5 })
{
for (int i = 0; i < vec.Length; i++)
{
TimerTask? current = vec[i];
while (current != null)
{
if (current.Id == id)
{
if (Interlocked.CompareExchange(ref current.State, 2, 1) == 1)
{
RemoveTask(ref vec[i], current);
this.ReleaseTask(current);
return true;
}

return false;
}

current = current.Next;
}
}
}

return false;
}
}

public void Dispose() { }

public void Tick()
{
lock (this.syncRoot)
{
this.TimeWheelWorker();
}
}

#endregion

#region Core Algorithm

private void TimeWheelWorker()
{
if (this.currentTick >= long.MaxValue - 1)
this.currentTick = 0;
else
this.currentTick++;

int index = (int)(this.currentTick & TVR_MASK);
if (index == 0)
{
index = (int)(this.currentTick >> TVR_BITS) & TVN_MASK;
this.CascadeTimers(this.tv2, index);
if (index == 0)
{
index = (int)((this.currentTick >> (TVR_BITS + TVN_BITS)) & TVN_MASK);
this.CascadeTimers(this.tv3, index);
if (index == 0)
{
index = (int)((this.currentTick >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK);
this.CascadeTimers(this.tv4, index);
if (index == 0)
{
index = (int)((this.currentTick >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK);
this.CascadeTimers(this.tv5, index);
}
}
}
}

this.ProcessTimers(ref this.tv1[this.currentTick & TVR_MASK]);
}

private void CascadeTimers(TimerTask?[] tv, int index)
{
TimerTask? current = tv[index];
tv[index] = null;

while (current != null)
{
TimerTask? next = current.Next;
this.InternalAddTask(current);
current = next;
}
}

private void ProcessTimers(ref TimerTask? head)
{
var taskList = this.taskListCache;
ExtractTasks(ref head, taskList);
if (taskList.Count == 0)
return;

if (taskList.Count > 1 && this.shouldSortBeforeExecution)
taskList.Sort((a, b) => a.ExecuteOrder.CompareTo(b.ExecuteOrder));

foreach (var task in taskList)
{
if (task.State == 1 && this.currentTick >= task.ExpireTicks)
{
try
{
task.Callback?.Invoke(task.Data);
}
catch (Exception ex)
{
if (this.OnCallbackError != null)
{
this.OnCallbackError.Invoke(ex);
}
else
{
throw;
}
}

if (task.RepeatCount != 0)
{
if (task.RepeatCount > 0) task.RepeatCount--;

task.ExpireTicks += task.Interval;
task.ExecuteOrder = Interlocked.Increment(ref this.executeOrder);

// 重新加入时间轮
lock (this.syncRoot)
{
this.InternalAddTask(task);
}
}
else
{
this.ReleaseTask(task);
}
}
else if (task.State == 2)
{
this.ReleaseTask(task);
}
}

taskList.Clear();
}

private static void ExtractTasks(ref TimerTask? head, List<TimerTask> taskList)
{
TimerTask? current = head;
while (current != null)
{
var next = current.Next;
taskList.Add(current);
current.Next = null;
current.Prev = null;
current = next;
}

head = null; // 清空原链表
}

private void InternalAddTask(TimerTask task)
{
// 如果任务已经被取消(State==2),则不再添加
if (Interlocked.CompareExchange(ref task.State, 1, 1) == 2)
return;

var expires = task.ExpireTicks;
var idx = expires - this.currentTick;

int i;
TimerTask?[] vec;
if (idx < TVR_SIZE)
{
i = (int)(expires & TVR_MASK);
vec = this.tv1;
}
else if (idx < (1 << (TVR_BITS + TVN_BITS)))
{
i = (int)((expires >> TVR_BITS) & TVN_MASK);
vec = this.tv2;
}
else if (idx < (1 << (TVR_BITS + 2 * TVN_BITS)))
{
i = (int)((expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK);
vec = this.tv3;
}
else if (idx < (1 << (TVR_BITS + 3 * TVN_BITS)))
{
i = (int)((expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK);
vec = this.tv4;
}
else
{
i = (int)((expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK);
vec = this.tv5;
}

// 清理指针,避免旧指向造成环
task.Prev = null;
task.Next = null;

var oldTask = vec[i];
if (oldTask != null)
{
oldTask.Prev = task;
task.Next = oldTask;
}

vec[i] = task;
// State 应设置为 1,表示激活,之前判断过 2 的情况已经返回
task.State = 1;
}

#endregion

#region Memory Management

private TimerTask AllocateTask()
{
lock (this.taskPool)
{
return this.taskPool.Count > 0 ? this.taskPool.Pop()! : new();
}
}

private void ReleaseTask(TimerTask task)
{
// 标记为已取消,避免在并发期间被重新加入
Interlocked.Exchange(ref task.State, 2);

task.Id = 0;
task.ExecuteOrder = 0;
task.Callback = null;
task.Data = null;
task.ExpireTicks = 0;
task.Interval = 0;
task.RepeatCount = 0;
task.Next = null;
task.Prev = null;
task.State = 0;

lock (this.taskPool)
{
if (this.taskPool.Count < 1024) this.taskPool.Push(task);
}
}

#endregion

#region Helpers

private static void RemoveTask(ref TimerTask? head, TimerTask task)
{
if (task.Prev != null)
task.Prev.Next = task.Next;
else
head = task.Next;

if (task.Next != null)
task.Next.Prev = task.Prev;

// 断开被移除节点的指针,避免其他线程看到旧链接
task.Next = null;
task.Prev = null;
}

#endregion
}