├── .gitignore ├── README.md ├── README.md.meta ├── Scripts.meta └── Scripts ├── TimingWheel.cs └── TimingWheel.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimingWheel 2 | 分层时间轮 3 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1fe1ba38e0fe57a40b263cb60c3294ae 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b3f0659cbe5ee34897a546cb45bb431 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/TimingWheel.cs: -------------------------------------------------------------------------------- 1 | #region 注 释 2 | 3 | /*** 4 | * 5 | * Title: 6 | * 7 | * Description: 8 | * 9 | * Date: 10 | * Version: 11 | * Writer: 半只龙虾人 12 | * Github: https://github.com/haloman9527 13 | * Blog: https://www.haloman.net/ 14 | * 15 | */ 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using Atom; 22 | 23 | namespace Atom 24 | { 25 | public partial class TimingWheel : IDisposable 26 | { 27 | static TimingWheel() 28 | { 29 | ObjectPoolManager.RegisterPool(new LinkListNodePool()); 30 | } 31 | 32 | /// 33 | /// 插槽 34 | /// 35 | private Slot[] slots; 36 | 37 | /// 38 | /// 插槽数量 39 | /// 40 | private int slotCount; 41 | 42 | /// 43 | /// 时间刻度 44 | /// 45 | private long tickSpan; 46 | 47 | /// 48 | /// 时间轮转动一圈的时间 49 | /// 50 | private long wheelSpan; 51 | 52 | /// 53 | /// 当前时间轮的开始时间 54 | /// 55 | private long startTime; 56 | 57 | /// 58 | /// 当前指针时间 59 | /// 60 | private long currentTime; 61 | 62 | /// 63 | /// 当前时间戳 64 | /// 65 | private long currentTimeStamp; 66 | 67 | /// 68 | /// 当前指针下标 69 | /// 70 | private int currentIndicator; 71 | 72 | /// 73 | /// 正在执行事件 74 | /// 75 | private bool executingTask; 76 | 77 | /// 78 | /// 整个分层事件轮共享数据 79 | /// 80 | private TimingWheelSharedInfo sharedInfo; 81 | 82 | /// 83 | /// 外轮(刻度更大的时间轮) 84 | /// 85 | private TimingWheel outerWheel; 86 | 87 | /// 88 | /// 内轮(刻度更小的时间轮) 89 | /// 90 | private TimingWheel innerWheel; 91 | 92 | /// 93 | /// 当前指针时间 94 | /// 95 | public long CurrentTime 96 | { 97 | get { return currentTime; } 98 | } 99 | 100 | /// 101 | /// 当前时间戳 102 | /// 103 | public long CurrentTimeStamp 104 | { 105 | get { return currentTimeStamp; } 106 | } 107 | 108 | /// 109 | /// 插槽数量 110 | /// 111 | public int SlotCount 112 | { 113 | get { return slotCount; } 114 | } 115 | 116 | /// 117 | /// 时间刻度 118 | /// 119 | public long TickSpan 120 | { 121 | get { return tickSpan; } 122 | } 123 | 124 | /// 125 | /// 一周时间 126 | /// 127 | public long WheelSpan 128 | { 129 | get { return wheelSpan; } 130 | } 131 | 132 | /// 133 | /// 134 | /// 135 | /// 插槽数量 136 | /// 时间刻度 137 | /// 开始时间 138 | public TimingWheel(int slotCount, long tickSpan, long startTime) 139 | { 140 | this.tickSpan = tickSpan; 141 | this.slotCount = slotCount; 142 | this.wheelSpan = tickSpan * slotCount; 143 | this.startTime = startTime; 144 | this.currentTimeStamp = startTime; 145 | this.currentTime = startTime; 146 | this.slots = new Slot[slotCount]; 147 | this.sharedInfo = new TimingWheelSharedInfo(); 148 | for (int i = 0; i < slotCount; i++) 149 | { 150 | slots[i] = new Slot(); 151 | } 152 | } 153 | 154 | private void Tick_Internal() 155 | { 156 | if (currentIndicator == 0) 157 | { 158 | outerWheel?.Tick_Internal(); 159 | } 160 | 161 | currentTime += tickSpan; 162 | currentIndicator = (currentIndicator + 1) % slots.Length; 163 | 164 | if (currentIndicator == 0) 165 | { 166 | startTime += wheelSpan; 167 | } 168 | 169 | var currentSlot = slots[currentIndicator]; 170 | var taskCount = currentSlot.tasks.Count; 171 | var taskNode = currentSlot.tasks.First; 172 | 173 | while (taskNode != null && taskCount-- > 0) 174 | { 175 | var task = taskNode.Value; 176 | var taskInfo = sharedInfo.taskInfos[task]; 177 | 178 | if (currentTime >= taskInfo.nextTime) 179 | { 180 | if (innerWheel != null) 181 | { 182 | RemoveTask_Internal(task); 183 | innerWheel.AddTask_Internal(task, taskInfo.nextTime); 184 | } 185 | else 186 | { 187 | RemoveTask_Internal(task); 188 | 189 | try 190 | { 191 | task.Invoke(this); 192 | } 193 | catch (Exception e) 194 | { 195 | throw e; 196 | } 197 | 198 | if (task.LoopTime < 0 || --task.LoopTime > 0) 199 | { 200 | taskInfo.nextTime += task.LoopInterval; 201 | AddTask_Internal(task, taskInfo.nextTime); 202 | } 203 | } 204 | } 205 | 206 | taskNode = taskNode.Next; 207 | } 208 | } 209 | 210 | /// 211 | /// 212 | /// 213 | /// 214 | /// must be bigger than current time 215 | private void AddTask_Internal(ITimeTask task, long nextTime) 216 | { 217 | var delta = nextTime - currentTime; 218 | if (delta <= 0) 219 | { 220 | try 221 | { 222 | task.Invoke(this); 223 | } 224 | catch (Exception e) 225 | { 226 | throw e; 227 | } 228 | 229 | if (task.LoopTime < 0 || --task.LoopTime > 0) 230 | { 231 | nextTime += task.LoopInterval; 232 | AddTask_Internal(task, nextTime); 233 | } 234 | } 235 | else if (delta <= wheelSpan) 236 | { 237 | if (delta < tickSpan && innerWheel != null) 238 | { 239 | innerWheel.AddTask_Internal(task, nextTime); 240 | } 241 | else 242 | { 243 | var offset = nextTime - startTime; 244 | var index = ((offset / tickSpan) + (offset % tickSpan > 0 ? 1 : 0)) % slotCount; 245 | var slot = slots[index]; 246 | var taskNode = (LinkedListNode)ObjectPoolManager.Spawn(typeof(LinkedListNode)); 247 | taskNode.Value = task; 248 | slot.tasks.AddLast(taskNode); 249 | 250 | var taskInfo = new TimeTaskInfo(); 251 | taskInfo.nextTime = nextTime; 252 | taskInfo.wheel = this; 253 | taskInfo.slot = slot; 254 | taskInfo.linkListNode = taskNode; 255 | sharedInfo.taskInfos[task] = taskInfo; 256 | } 257 | } 258 | else if (delta > wheelSpan) 259 | { 260 | outerWheel?.AddTask_Internal(task, nextTime); 261 | } 262 | } 263 | 264 | private void RemoveTask_Internal(ITimeTask task) 265 | { 266 | var taskInfo = sharedInfo.taskInfos[task]; 267 | sharedInfo.taskInfos.Remove(task); 268 | taskInfo.slot.tasks.Remove(taskInfo.linkListNode); 269 | ObjectPoolManager.Recycle(taskInfo.linkListNode); 270 | } 271 | 272 | /// 273 | /// 274 | /// 275 | /// 父轮插槽数量 276 | public void BuildParent(params int[] parentSlotCounts) 277 | { 278 | if (parentSlotCounts == null) 279 | return; 280 | 281 | var wheel = this; 282 | for (int i = 0; i < parentSlotCounts.Length; i++) 283 | { 284 | if (wheel.outerWheel != null) 285 | { 286 | wheel = outerWheel; 287 | } 288 | else 289 | { 290 | wheel.outerWheel = new TimingWheel(parentSlotCounts[i], wheelSpan, startTime); 291 | wheel.outerWheel.innerWheel = this; 292 | wheel.outerWheel.sharedInfo = this.sharedInfo; 293 | wheel = wheel.outerWheel; 294 | } 295 | } 296 | } 297 | 298 | /// 299 | /// 推进时间轮,前进1刻度() 300 | /// 301 | public void Tick() 302 | { 303 | currentTimeStamp += tickSpan; 304 | var delta = currentTimeStamp - currentTime; 305 | while (delta > tickSpan) 306 | { 307 | Tick_Internal(); 308 | delta -= tickSpan; 309 | } 310 | } 311 | 312 | /// 推进时间轮 313 | /// 拨动指针,前进一段时间 314 | public void Tick(long tick) 315 | { 316 | currentTimeStamp += tick; 317 | var delta = currentTimeStamp - currentTime; 318 | while (delta > tickSpan) 319 | { 320 | Tick_Internal(); 321 | delta -= tickSpan; 322 | } 323 | } 324 | 325 | public bool ContainsTask(ITimeTask task) 326 | { 327 | return sharedInfo.taskInfos.ContainsKey(task); 328 | } 329 | 330 | /// 331 | /// 添加时间任务 332 | /// 333 | /// 334 | /// 335 | public void AddTask(ITimeTask task, long startDelay = 0) 336 | { 337 | if (sharedInfo.taskInfos.ContainsKey(task)) 338 | { 339 | return; 340 | } 341 | 342 | startDelay = Math.Max(startDelay, 0); 343 | var nextInvokeTime = currentTime + startDelay; 344 | 345 | AddTask_Internal(task, nextInvokeTime); 346 | } 347 | 348 | public void RemoveTask(ITimeTask task) 349 | { 350 | if (!sharedInfo.taskInfos.TryGetValue(task, out var taskInfo)) 351 | { 352 | return; 353 | } 354 | 355 | RemoveTask_Internal(task); 356 | } 357 | 358 | public void ClearTasks() 359 | { 360 | foreach (var pair in sharedInfo.taskInfos) 361 | { 362 | var taskInfo = pair.Value; 363 | taskInfo.slot.tasks.Remove(taskInfo.linkListNode); 364 | ObjectPoolManager.Recycle(taskInfo.linkListNode); 365 | } 366 | 367 | sharedInfo.taskInfos.Clear(); 368 | } 369 | 370 | public void Dispose() 371 | { 372 | ClearTasks(); 373 | } 374 | } 375 | 376 | 377 | public partial class TimingWheel 378 | { 379 | public interface ITimeTask 380 | { 381 | /// 382 | /// 循环次数 383 | /// -1: 无限 384 | /// 0|1: 不循环 385 | /// 386 | int LoopTime { get; set; } 387 | 388 | /// 389 | /// 循环间隔 390 | /// 391 | long LoopInterval { get; set; } 392 | 393 | void Invoke(TimingWheel timingWheel); 394 | } 395 | 396 | public class CustomTask : ITimeTask 397 | { 398 | public int LoopTime { get; set; } 399 | 400 | public long LoopInterval { get; set; } 401 | 402 | public Action task; 403 | 404 | public void Invoke(TimingWheel timingWheel) 405 | { 406 | task?.Invoke(); 407 | } 408 | } 409 | 410 | public class DayTask : ITimeTask 411 | { 412 | public int LoopTime { get; set; } 413 | 414 | public long LoopInterval 415 | { 416 | get => 86400000L; 417 | set { } 418 | } 419 | 420 | public Action task; 421 | 422 | public void Invoke(TimingWheel timingWheel) 423 | { 424 | task?.Invoke(); 425 | } 426 | } 427 | 428 | public class WeekTask : ITimeTask 429 | { 430 | public int LoopTime { get; set; } 431 | 432 | public long LoopInterval 433 | { 434 | get => 604800000L; 435 | set { } 436 | } 437 | 438 | public Action task; 439 | 440 | public void Invoke(TimingWheel timingWheel) 441 | { 442 | task?.Invoke(); 443 | } 444 | } 445 | 446 | public class MonthTask : ITimeTask 447 | { 448 | public int LoopTime { get; set; } 449 | 450 | public long LoopInterval 451 | { 452 | get => new DateTimeOffset(0, 1, 0, 0, 0, 0, TimeSpan.Zero).Millisecond; 453 | set { } 454 | } 455 | 456 | public Action task; 457 | 458 | public void Invoke(TimingWheel timingWheel) 459 | { 460 | task?.Invoke(); 461 | } 462 | } 463 | 464 | public class YearTask : ITimeTask 465 | { 466 | public int LoopTime { get; set; } 467 | 468 | public long LoopInterval 469 | { 470 | get => new DateTimeOffset(1, 0, 0, 0, 0, 0, TimeSpan.Zero).Millisecond; 471 | set { } 472 | } 473 | 474 | public Action task; 475 | 476 | public void Invoke(TimingWheel timingWheel) 477 | { 478 | task?.Invoke(); 479 | } 480 | } 481 | 482 | public struct TimeTaskInfo 483 | { 484 | /// 485 | /// 任务下次执行时间 486 | /// 487 | public long nextTime; 488 | 489 | /// 490 | /// 任务所在时间层 491 | /// 492 | public TimingWheel wheel; 493 | 494 | /// 495 | /// 任务所在刻度 496 | /// 497 | public Slot slot; 498 | 499 | /// 500 | /// 任务所在的链表的位置 501 | /// 502 | public LinkedListNode linkListNode; 503 | } 504 | 505 | /// 506 | /// 整个分层事件轮中共享的数据 507 | /// 508 | internal class TimingWheelSharedInfo 509 | { 510 | /// 511 | /// 任务数据 512 | /// 513 | public Dictionary taskInfos = new Dictionary(); 514 | } 515 | 516 | public class Slot 517 | { 518 | public LinkedList tasks = new LinkedList(); 519 | } 520 | 521 | public class LinkListNodePool : ObjectPoolBase> 522 | { 523 | protected override LinkedListNode Create() 524 | { 525 | return new LinkedListNode(default); 526 | } 527 | } 528 | } 529 | } -------------------------------------------------------------------------------- /Scripts/TimingWheel.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7c9a3470a15996b4aad63ee2e501feb7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------