抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

我们在开发的过程中,有时候需要一个行为延迟一定时间,或者是重复执行,这时候我们就需要用到定时器。Unity 没有专门的定时器系统,不过有协程的方式来实现这一行为。不过协程的使用难免有些不方便,也有一些性能上的消耗,因此决定自己造轮子。本系统使用时间轮的思想构建定时器系统,有别于传统的全遍历方式。

原理

时间轮

网图:

  1. 假设我们有一个时间轮盘,这个轮盘有好几个槽位,每个槽位里面有一个链表用于保存符合当前条件的定时任务。
  2. 每个定时任务放入对应槽位的规则为 当前时间 + 延迟时间 + 每次循环时间,这样每个槽位就对应着触发任务的时间。
  3. 我们按照当前时间来访问对应的槽位,即到了需要触发任务的时间,任务触发。

如果我们按照一个最小时间间隔,每个时间都开设一个槽位的话,需要的存储空间就太大了。因此我们需要多层时间轮,就好像水表有好几个转盘表示不同的刻度。我们把时间分为年(需要的话)、月、日、时、分、毫秒,本例中没有年轮,总共是六个时间轮。毫秒轮的数量太大,我们也可以按照设定的最小时间间隔减少划分(本例按照 50ms 为最小粒度划分 20 个槽位)。本例中时间轮用字典保存,任务列表用链表保存。

当我们的任务为定时到 5 秒后,我们就把当前任务放入月轮当月的槽位,然后我们在轮询的时候按照六个符合当前时间的时间轮槽位依次遍历链表。由于我们每次添加任务都是添加到最顶层的月轮,因此我们把当前时间的月轮槽位中的所有任务都取出并添加到日轮。日轮也是同样的操作,依次执行之后任务就到达毫秒轮中。

任务最终到达毫秒轮后,我们就遍历当前毫秒轮槽位的链表,比较当前时间和任务目标时间,大于等于的情况下执行任务并移除出链表。移除之后,我们还要判断当前任务是否需要循环,需要循环的话我们把当前任务又重新添加到月轮中,执行下一个循环。

链式调用

为了让定时器更加实用,我们引入了链式调用。我们在创建定时器的时候会返回一个链式调用的对象,这样就可以在创建一次定时器之后再点出一个新的定时器。这个新的定时器的延迟时间为前面所有定时器的总经过时间再加上自己的延迟时间,这样就可以避免定时器的嵌套。

使用方式

使用之前先调用 TimerUtils.Init() 来初始化,结束之后调用 TimerUtils.Stop() 来结束。

调用定时器的方式分为两种,一种是同步任务,一种是异步任务。由于 Unity 对象只有在主线程上才能访问,因此如果要延迟操作 Unity 对象的话就需要用同步任务,其他情况可以选择异步调用,根据需求即可。

  • 延迟执行:TimerChain Once(int delay, Action action)
    下面的例子是延迟一秒后打印消息到控制台。

    1
    2
    3
    4
    TimerUtils.Once(1000, () =>
    {
    ConsoleUtils.Log("执行", DateTime.Now, chain.GetId());
    });
  • 循环执行:TimerChain Loop(int interval, Action action, int delay = 0, int loopTimes = -1)
    下面的例子是循环三次,每次 1 秒间隔,延迟时间为 0,不传入循环次数就是无限循环。

    1
    2
    3
    4
    TimerUtils.Loop(1000, () =>
    {
    ConsoleUtils.Log("循环", DateTime.Now);
    }, 0, 3);
  • 清除定时器

    1
    2
    3
    4
    5
    6
    7
    8
    TimerChain chain = TimerUtils.Loop(1000, () =>
    {
    ConsoleUtils.Log("循环", DateTime.Now);
    }, 0, 3);
    //清除定时器
    chain.Clear();
    //或者用下面的方式,两种都一样
    TimerUtils.Clear(chain);
  • 链式调用

    1
    2
    3
    4
    5
    6
    7
    TimerUtils.Loop(1000, () =>
    {
    ConsoleUtils.Log("循环", DateTime.Now);
    }, 0, 3).Once(5000, () =>
    {
    ConsoleUtils.Log("等待", DateTime.Now);
    });

异步的调用和同步的一样,只是函数名不同(带 Async)。

代码

TimerTask
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
using System;
using System.Threading.Tasks;

public class TimerTask
{
public int id;

public int delay;

public int interval;

public int loopTimes;

public Action action;

public DateTime dateTime;

public TimerType type;

public bool isRemove;

public TimerTask(int id, int interval, Action action, int loopTimes, int delay, TimerType type)
{
this.id = id;
this.interval = interval;
this.action = action;
this.loopTimes = loopTimes;
this.delay = delay;
this.type = type;
isRemove = false;
dateTime = DateTime.Now.AddMilliseconds(interval + delay);
}

public void Run()
{
if (type == TimerType.Sync)
{
TimerUtils.AddAction(id, action);
}
else
{
RunAsync();
}
}

public async Task RunAsync()
{
await Task.Run(() =>
{
action.Invoke();
});
}

public bool CheckLoop()
{
if (isRemove)
{
return false;
}
if (loopTimes < 0)
{
dateTime = DateTime.Now.AddMilliseconds(interval);
return true;
}
else
{
loopTimes--;
dateTime = DateTime.Now.AddMilliseconds(interval);
return loopTimes > 0;
}
}
}
TimerChain
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
using System;

public class TimerChain
{
private TimeWheel _timeWheel;

//private int _id = -1;

private TimerTask _task;

private int _delay = 0;

public TimerChain(TimeWheel wheel)
{
_timeWheel = wheel;
}

public TimerChain Once(int delay, Action action)
{
_task = _timeWheel.SetTimeout(_task != null ? _task.id : -1, delay + _delay, action);
_delay = delay;
return this;
}

public TimerChain Loop(int interval, Action action, int delay = 0, int loopTimes = -1)
{
_task = _timeWheel.SetInterval(_task != null ? _task != null ? _task.id : -1 : -1, interval, action, delay + _delay, loopTimes); ;
_delay = interval * loopTimes + delay;
return this;
}

public TimerChain OnceAsync(int delay, Action action)
{
_task = _timeWheel.SetTimeoutAsync(_task != null ? _task.id : -1, delay + _delay, action);
_delay = delay;
return this;
}

public TimerChain LoopAsync(int interval, Action action, int delay = 0, int loopTimes = -1)
{
_task = _timeWheel.SetIntervalAsync(_task != null ? _task.id : -1, interval, action, delay + _delay, loopTimes);
_delay = interval * loopTimes + delay;
return this;
}

public TimerChain Clear()
{
_task.isRemove = true;
_timeWheel.ClearInterval(_task != null ? _task.id : -1, true);
//ConsoleUtils.Log("清除任务Chain", _task.id);
return this;
}

public int GetId()
{
return _task != null ? _task.id : -1;
}
}
TimerUtils
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
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

namespace Timer
{
public class TimerUtils
{
private static TimeWheel _timeWheel;
private static Thread _thread;
private static bool _isRunning = false;
private static TimerScript _timerScript;

private static Dictionary<string, Dictionary<int, TimerChain>> _timerNodes =
new Dictionary<string, Dictionary<int, TimerChain>>();

public static void Init()
{
_timeWheel = new TimeWheel();
_isRunning = true;

#if UNITY_WEBGL
Debug.Log("当前是WEBGL平台");
#else
Debug.Log("当前不是WEBGL平台");
_thread = new Thread(Update);
_thread.Start();
#endif

GameObject obj = new GameObject();
obj.name = "TimerUtils";
_timerScript = obj.AddComponent<TimerScript>();
}

public static void Stop()
{
_isRunning = false;
//_thread.Abort();
}

public static void Update()
{
#if UNITY_WEBGL
_timeWheel.Update();
#else
while (_isRunning)
{
_timeWheel.Update();
}
#endif
}

/// <summary>
/// 同步定时器
/// </summary>
/// <param name="id">节点id</param>
/// <param name="delay">延时 单位毫秒</param>
/// <param name="action">行为</param>
/// <returns></returns>
public static TimerChain Once(string id,int delay, Action action)
{
var timeChain = new TimerChain(_timeWheel);

void Callback()
{
Remove(id,timeChain.GetId());
action?.Invoke();
}

timeChain.Once(delay, Callback);

if (!_timerNodes.ContainsKey(id))
{
_timerNodes.Add(id,new Dictionary<int, TimerChain>());
}

_timerNodes[id][timeChain.GetId()] = timeChain;

return timeChain;
}

/// <summary>
/// 同步循环 第一次触发时间为 interval + delay
/// </summary>
/// <param name="id">节点id</param>
/// <param name="interval">循环间隔时间</param>
/// <param name="action">行为</param>
/// <param name="delay">延时</param>
/// <param name="loopTimes">循环次数 默认为-1 无限循环</param>
/// <returns></returns>
public static TimerChain Loop(string id,int interval, Action action, int delay = 0, int loopTimes = -1)
{
var timeChain = new TimerChain(_timeWheel);

void Callback()
{
Remove(id,timeChain.GetId());
action?.Invoke();
}

timeChain.Loop(interval, Callback, delay, loopTimes);

if (!_timerNodes.ContainsKey(id))
{
_timerNodes.Add(id,new Dictionary<int, TimerChain>());
}

_timerNodes[id][timeChain.GetId()] = timeChain;
return timeChain;
}

/// <summary>
/// 异步定时器
/// </summary>
/// <param name="id">节点id</param>
/// <param name="delay">延时 单位毫秒</param>
/// <param name="action">行为</param>
/// <returns></returns>
public static TimerChain OnceAsync(string id,int delay, Action action)
{
var timeChain = new TimerChain(_timeWheel);

void Callback()
{
Remove(id,timeChain.GetId());
action?.Invoke();
}

timeChain.OnceAsync(delay, Callback);

if (!_timerNodes.ContainsKey(id))
{
_timerNodes.Add(id,new Dictionary<int, TimerChain>());
}

_timerNodes[id][timeChain.GetId()] = timeChain;

return timeChain;
}


/// <summary>
/// 异步循环 第一次触发时间为 interval + delay
/// </summary>
/// <param name="id">节点id</param>
/// <param name="interval">循环间隔时间</param>
/// <param name="action">行为</param>
/// <param name="delay">延时</param>
/// <param name="loopTimes">循环次数 默认为-1 无限循环</param>
/// <returns></returns>
public static TimerChain LoopAsync(string id,int interval, Action action, int delay = 0, int loopTimes = -1)
{
var timeChain = new TimerChain(_timeWheel);

void Callback()
{
Remove(id,timeChain.GetId());
action?.Invoke();
}

timeChain.LoopAsync(interval, Callback, delay, loopTimes);

if (!_timerNodes.ContainsKey(id))
{
_timerNodes.Add(id,new Dictionary<int, TimerChain>());
}

_timerNodes[id][timeChain.GetId()] = timeChain;
return timeChain;
}

public static TimerChain Clear(TimerChain chain)
{
chain.Clear();
return chain;
}

private static void Remove(string id,int timerChainId)
{
_timerNodes[id].Remove(timerChainId);
}

/// <summary>
/// 移除节点上的所有定时器
/// </summary>
/// <param name="id"></param>
public static void ClearAll(string id)
{
if (_timerNodes.ContainsKey(id))
{
foreach (var data in _timerNodes[id])
{
Clear(data.Value);
}

_timerNodes.Remove(id);
}
}

public static void AddAction(int id, Action action)
{
_timerScript.AddAction(id, action);
}
}
}
TimeWheel
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
using System;
using System.Collections.Generic;
using System.Linq;

public class TimeWheel
{
private Dictionary<int, LinkedList<TimerTask>> _month = new Dictionary<int, LinkedList<TimerTask>>();
private Dictionary<int, LinkedList<TimerTask>> _day = new Dictionary<int, LinkedList<TimerTask>>();
private Dictionary<int, LinkedList<TimerTask>> _hour = new Dictionary<int, LinkedList<TimerTask>>();
private Dictionary<int, LinkedList<TimerTask>> _minute = new Dictionary<int, LinkedList<TimerTask>>();
private Dictionary<int, LinkedList<TimerTask>> _second = new Dictionary<int, LinkedList<TimerTask>>();
private Dictionary<int, LinkedList<TimerTask>> _millisecond = new Dictionary<int, LinkedList<TimerTask>>();
//private long _curTime = 0;
private int _id = 0;
private object _lock = new object();

public TimeWheel()
{
ConsoleUtils.Log(DateTime.Now);
for (int i = 0; i < 13; i++)
{
_month.Add(i, new LinkedList<TimerTask>());
}

for (int i = 0; i < 32; i++)
{
_day.Add(i, new LinkedList<TimerTask>());
}

for (int i = 0; i < 24; i++)
{
_hour.Add(i, new LinkedList<TimerTask>());
}

for (int i = 0; i < 60; i++)
{
_minute.Add(i, new LinkedList<TimerTask>());
}

for (int i = 0; i < 60; i++)
{
_second.Add(i, new LinkedList<TimerTask>());
}
//毫秒级粒度为50ms
for (int i = 0; i < 20; i++)
{
_millisecond.Add(i, new LinkedList<TimerTask>());
}
}

public void Update()
{
lock (_lock)
{
DateTime now = DateTime.Now;

LinkedList<TimerTask> month = _month[now.Month];
LinkedList<TimerTask> day = _day[now.Day];
LinkedList<TimerTask> hour = _hour[now.Hour];
LinkedList<TimerTask> minute = _minute[now.Minute];
LinkedList<TimerTask> second = _second[now.Second];

int milliSecondDelta = now.Millisecond / 50;
LinkedList<TimerTask> millisecond = _millisecond[milliSecondDelta];

while (month.Count > 0)
{
LinkedListNode<TimerTask> node = month.First;
month.RemoveFirst();
//添加到日轮
_day[node.Value.dateTime.Day].AddLast(node);
}

while (day.Count > 0)
{
LinkedListNode<TimerTask> node = day.First;
day.RemoveFirst();
//添加到小时轮
_hour[node.Value.dateTime.Hour].AddLast(node);
}

while (hour.Count > 0)
{
LinkedListNode<TimerTask> node = hour.First;
hour.RemoveFirst();
//添加到分轮
_minute[node.Value.dateTime.Minute].AddLast(node);
}

while (minute.Count > 0)
{
LinkedListNode<TimerTask> node = minute.First;
minute.RemoveFirst();
//添加到秒轮
_second[node.Value.dateTime.Second].AddLast(node);
}

while (second.Count > 0)
{
LinkedListNode<TimerTask> node = second.First;
second.RemoveFirst();
//添加到毫秒轮
_millisecond[node.Value.dateTime.Millisecond / 50].AddLast(node);
}

while (millisecond.Count > 0)
{
//LinkedListNode<TimerTask> node = second.First;
//second.RemoveFirst();
////添加到毫秒轮
//_millisecond[node.Value.dateTime.Millisecond / 50].AddLast(node);

foreach (var task in millisecond.ToList())
{
if (task != null && DateTime.Now >= task.dateTime)
{
millisecond.Remove(task);

//ConsoleUtils.Log("执行任务", task.id);
//task.RunAsync();
task.Run();

if (task.CheckLoop())
{
AddTask(task);
}
}
}
}
}
}

public TimerTask SetInterval(int id, int interval, Action action, int delay, int loopTimes, bool isRemove = false)
{
if (id == -1)
{
id = GetId();
}
TimerTask task = new TimerTask(id, interval, action, loopTimes, delay, TimerType.Sync);
AddTask(task);
return task;
}

public TimerTask SetTimeout(int id, int delay, Action action)
{
if (id == -1)
{
id = GetId();
}
TimerTask task = new TimerTask(id, 0, action, 1, delay, TimerType.Sync);
AddTask(task);
return task;
}

public TimerTask SetIntervalAsync(int id, int interval, Action action, int delay, int loopTimes)
{
if (id == -1)
{
id = GetId();
}
TimerTask task = new TimerTask(id, interval, action, loopTimes, delay, TimerType.Async);
AddTask(task);
return task;
}

public TimerTask SetTimeoutAsync(int id, int delay, Action action)
{
if (id == -1)
{
id = GetId();
}
TimerTask task = new TimerTask(id, 0, action, 1, delay, TimerType.Async);
AddTask(task);
return task;
}

public void ClearInterval(int id, bool isAll = false)
{
if (isAll)
{
RemoveTask(_month, id, isAll);
RemoveTask(_day, id, isAll);
RemoveTask(_hour, id, isAll);
RemoveTask(_minute, id, isAll);
RemoveTask(_second, id, isAll);
RemoveTask(_millisecond, id, isAll);
}
else
{
if (RemoveTask(_month, id))
{
return;
}

if (RemoveTask(_day, id))
{
return;
}

if (RemoveTask(_hour, id))
{
return;
}

if (RemoveTask(_minute, id))
{
return;
}

if (RemoveTask(_second, id))
{
return;
}

if (RemoveTask(_millisecond, id))
{
return;
}
}
}

private void AddTask(TimerTask task)
{
lock(_lock){
_month[task.dateTime.Month].AddLast(task);
}
}

private bool RemoveTask(Dictionary<int, LinkedList<TimerTask>> wheel, int id, bool isAll = false)
{
if (isAll)
{
bool res = false;
foreach (var item in wheel)
{
LinkedList<TimerTask> tasks = item.Value;
foreach (var task in tasks.ToList())
{
if (task.id == id)
{
//ConsoleUtils.Log("清除任务", task.id, task.isRemove);
tasks.Remove(task);
res = true;
}
}
}
return res;
}
else
{
foreach (var item in wheel)
{
LinkedList<TimerTask> tasks = item.Value;
foreach (var task in tasks)
{
if (task.id == id)
{
tasks.Remove(task);
return true;
}
}
}
return false;
}
}

private int GetId()
{
return _id++;
}
}
TimerScript
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
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using UnityEngine;

public class TimerScript : MonoBehaviour
{
private Queue<(int, Action)> _actions = new Queue<(int, Action)>();

private ConcurrentDictionary<int, bool> _register = new ConcurrentDictionary<int, bool>();

void Update()
{
while (_actions.Count > 0)
{
(int, Action) item = _actions.Dequeue();
Run(item.Item2);
_register.TryRemove(item.Item1, out var res);
}
}

public void AddAction(int id, Action action)
{
if (!_register.ContainsKey(id))
{
_actions.Enqueue((id, action));
_register.TryAdd(id, true);
}
}

private void Run(Action action)
{
action.Invoke();
}
}

更新日志

2024-07-31

  1. 修复时间轮槽位 Bug。

2024-07-30

  1. 新增按节点存储所属定时器和按节点清除定时器功能。

2024-07-02

  1. 修复多线程 Bug。

2023-12-20

  1. 修复 bug

2023-12-20

  1. 更新基础版本。

评论