├── .gitignore ├── HWT ├── HWT.csproj ├── HashedWheelBucket.cs ├── HashedWheelTimeout.cs ├── HashedWheelTimer.cs ├── TimedAwaiter.cs ├── Timeout.cs ├── Timer.cs └── TimerTask.cs ├── HashWheelTimerApp.sln ├── HashWheelTimerApp ├── HashWheelTimerApp.csproj └── Program.cs ├── LICENSE ├── README.md ├── console.png └── hwt.png /.gitignore: -------------------------------------------------------------------------------- 1 | HashWheelTimerApp/obj/ 2 | HashWheelTimerApp/bin/ 3 | HWT/obj/ 4 | HWT/bin/ 5 | .vs -------------------------------------------------------------------------------- /HWT/HWT.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | HashedWheelTimer 6 | true 7 | EveryMatrix.Inc 8 | Jerry.Wang 9 | HashedWheelTimer implemented in C# and .Net Standard inspired by io.netty.util.HashedWheelTimer 10 | HWT 11 | 1.0.2 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HWT/HashedWheelBucket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace HWT 5 | { 6 | internal sealed class HashedWheelBucket 7 | { 8 | // Used for the linked-list datastructure 9 | private HashedWheelTimeout _head; 10 | private HashedWheelTimeout _tail; 11 | 12 | public void AddTimeout(HashedWheelTimeout timeout) 13 | { 14 | timeout._bucket = this; 15 | if (_head == null) 16 | { 17 | _head = _tail = timeout; 18 | } 19 | else 20 | { 21 | _tail._next = timeout; 22 | timeout._prev = _tail; 23 | _tail = timeout; 24 | } 25 | } 26 | 27 | /// 28 | /// Expire all HashedWheelTimeout for the given deadline. 29 | /// 30 | /// 31 | internal void ExpireTimeouts(long deadline) 32 | { 33 | HashedWheelTimeout timeout = _head; 34 | 35 | // process all timeouts 36 | while (timeout != null) 37 | { 38 | HashedWheelTimeout next = timeout._next; 39 | if (timeout._remainingRounds <= 0) 40 | { 41 | next = Remove(timeout); 42 | if (timeout._deadline <= deadline) 43 | { 44 | timeout.Expire(); 45 | } 46 | else 47 | { 48 | // The timeout was placed into a wrong slot. This should never happen. 49 | throw new InvalidOperationException($"timeout.deadline ({timeout._deadline}) > deadline ({deadline})"); 50 | } 51 | } 52 | else if (timeout.Cancelled) 53 | { 54 | next = Remove(timeout); 55 | } 56 | else 57 | { 58 | timeout._remainingRounds--; 59 | } 60 | timeout = next; 61 | } 62 | } 63 | 64 | internal HashedWheelTimeout Remove(HashedWheelTimeout timeout) 65 | { 66 | HashedWheelTimeout next = timeout._next; 67 | // remove timeout that was either processed or cancelled by updating the linked-list 68 | if (timeout._prev != null) 69 | { 70 | timeout._prev._next = next; 71 | } 72 | if (timeout._next != null) 73 | { 74 | timeout._next._prev = timeout._prev; 75 | } 76 | 77 | if (timeout == _head) 78 | { 79 | // if timeout is also the tail we need to adjust the entry too 80 | if (timeout == _tail) 81 | { 82 | _tail = null; 83 | _head = null; 84 | } 85 | else 86 | { 87 | _head = next; 88 | } 89 | } 90 | else if (timeout == _tail) 91 | { 92 | // if the timeout is the tail modify the tail to be the prev node. 93 | _tail = timeout._prev; 94 | } 95 | // null out prev, next and bucket to allow for GC. 96 | timeout._prev = null; 97 | timeout._next = null; 98 | timeout._bucket = null; 99 | timeout._timer.DescreasePendingTimeouts(); 100 | return next; 101 | } 102 | 103 | internal void ClearTimeouts(ISet set) 104 | { 105 | for (; ; ) 106 | { 107 | HashedWheelTimeout timeout = PollTimeout(); 108 | if (timeout == null) 109 | { 110 | return; 111 | } 112 | if (timeout.Expired || timeout.Cancelled) 113 | { 114 | continue; 115 | } 116 | set.Add(timeout); 117 | } 118 | } 119 | 120 | private HashedWheelTimeout PollTimeout() 121 | { 122 | HashedWheelTimeout head = this._head; 123 | if (head == null) 124 | { 125 | return null; 126 | } 127 | HashedWheelTimeout next = head._next; 128 | if (next == null) 129 | { 130 | _tail = this._head = null; 131 | } 132 | else 133 | { 134 | this._head = next; 135 | next._prev = null; 136 | } 137 | 138 | // null out prev and next to allow for GC. 139 | head._next = null; 140 | head._prev = null; 141 | head._bucket = null; 142 | return head; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /HWT/HashedWheelTimeout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace HWT 6 | { 7 | internal sealed class HashedWheelTimeout : Timeout 8 | { 9 | internal const int ST_INIT = 0; 10 | internal const int ST_CANCELLED = 1; 11 | internal const int ST_EXPIRED = 2; 12 | 13 | private volatile int _state = ST_INIT; 14 | internal int State { get { return _state; } } 15 | 16 | internal readonly HashedWheelTimer _timer; 17 | private readonly TimerTask _task; 18 | internal readonly long _deadline; 19 | 20 | // remainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the 21 | // HashedWheelTimeout will be added to the correct HashedWheelBucket. 22 | internal long _remainingRounds; 23 | 24 | internal HashedWheelTimeout _next; 25 | internal HashedWheelTimeout _prev; 26 | 27 | // The bucket to which the timeout was added 28 | internal HashedWheelBucket _bucket; 29 | 30 | 31 | internal HashedWheelTimeout(HashedWheelTimer timer, TimerTask task, long deadline) 32 | { 33 | this._timer = timer; 34 | this._task = task; 35 | this._deadline = deadline; 36 | } 37 | 38 | 39 | public Timer Timer { get { return _timer; } } 40 | 41 | public TimerTask TimerTask { get { return _task; } } 42 | 43 | public bool Expired { get { return _state == ST_EXPIRED; } } 44 | 45 | public bool Cancelled { get { return _state == ST_CANCELLED; } } 46 | 47 | bool CompareAndSetState(int expected, int state) 48 | { 49 | int originalState = Interlocked.CompareExchange(ref _state, state, expected); 50 | return originalState == expected; 51 | } 52 | 53 | public bool Cancel() 54 | { 55 | // only update the state it will be removed from HashedWheelBucket on next tick. 56 | if (!CompareAndSetState(ST_INIT, ST_CANCELLED)) 57 | { 58 | return false; 59 | } 60 | // If a task should be canceled we put this to another queue which will be processed on each tick. 61 | // So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way 62 | // we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible. 63 | _timer._cancelledTimeouts.Enqueue(this); 64 | return true; 65 | } 66 | 67 | internal void Remove() 68 | { 69 | HashedWheelBucket bucket = _bucket; 70 | if (bucket != null) 71 | { 72 | bucket.Remove(this); 73 | } 74 | else 75 | { 76 | _timer.DescreasePendingTimeouts(); 77 | } 78 | } 79 | 80 | public void Expire() 81 | { 82 | if (!CompareAndSetState(ST_INIT, ST_EXPIRED)) 83 | { 84 | return; 85 | } 86 | 87 | Task.Run(() => { 88 | _task.Run(this); 89 | }); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /HWT/HashedWheelTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | 6 | 7 | namespace HWT 8 | { 9 | /// 10 | /// A Timer optimized for approximated I/O timeout scheduling. 11 | /// 12 | /// ## Tick Duration ## 13 | /// As described with 'approximated', this timer does not execute the scheduled TimerTask on time. HashedWheelTimer, 14 | /// on every tick, will check if there are any TimerTasks behind the schedule and execute them. 15 | /// You can increase or decrease the accuracy of the execution timing by specifying smaller or larger tick duration 16 | /// in the constructor.In most network applications, I/O timeout does not need to be accurate. 17 | /// Therefore, the default tick duration is 100 milliseconds and you will not need to try different configurations in most cases. 18 | /// 19 | /// ## Ticks per Wheel (Wheel Size) ## 20 | /// 21 | /// HashedWheelTimer maintains a data structure called 'wheel'. 22 | /// To put simply, a wheel is a hash table of TimerTasks whose hash function is 'dead line of the task'. 23 | /// The default number of ticks per wheel (i.e. the size of the wheel) is 512. 24 | /// You could specify a larger value if you are going to schedule a lot of timeouts. 25 | /// 26 | /// ## Do not create many instances. ## 27 | /// 28 | /// HashedWheelTimer creates a new thread whenever it is instantiated and started. 29 | /// Therefore, you should make sure to create only one instance and share it across your application. 30 | /// One of the common mistakes, that makes your application unresponsive, is to create a new instance for every connection. 31 | /// 32 | /// ## Implementation Details ## 33 | /// HashedWheelTimer is based on George Varghese and Tony Lauck's paper, 34 | /// 'Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility'. 35 | /// More comprehensive slides are located here http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt. 36 | /// 37 | public class HashedWheelTimer : Timer 38 | { 39 | public const int WORKER_STATE_INIT = 0; 40 | public const int WORKER_STATE_STARTED = 1; 41 | public const int WORKER_STATE_SHUTDOWN = 2; 42 | 43 | private volatile int _workerState; // 0 - init, 1 - started, 2 - shut down 44 | 45 | private readonly long _tickDuration; 46 | private readonly HashedWheelBucket[] _wheel; 47 | private readonly int _mask; 48 | private readonly ManualResetEvent _startTimeInitialized = new ManualResetEvent(false); 49 | private readonly ConcurrentQueue _timeouts = new ConcurrentQueue(); 50 | internal readonly ConcurrentQueue _cancelledTimeouts = new ConcurrentQueue(); 51 | private readonly long _maxPendingTimeouts; 52 | private readonly Thread _workerThread; 53 | 54 | /// 55 | /// There are 10,000 ticks in a millisecond 56 | /// 57 | private readonly long _base = DateTime.UtcNow.Ticks / 10000; 58 | 59 | 60 | private /*volatile*/ long _startTime; 61 | private long _pendingTimeouts = 0; 62 | 63 | private long GetCurrentMs() { return DateTime.UtcNow.Ticks / 10000 - _base; } 64 | 65 | internal long DescreasePendingTimeouts() 66 | { 67 | return Interlocked.Decrement(ref _pendingTimeouts); 68 | } 69 | 70 | /// 71 | /// Creates a new timer with default tick duration 100 ms, and default number of ticks per wheel 512. 72 | /// 73 | public HashedWheelTimer() : this(TimeSpan.FromMilliseconds(100), 512, 0) 74 | { 75 | } 76 | 77 | /// 78 | /// Creates a new timer. 79 | /// 80 | /// the duration between tick 81 | /// the size of the wheel 82 | /// The maximum number of pending timeouts after which call to NewTimeout will result in InvalidOperationException being thrown. No maximum pending timeouts limit is assumed if this value is 0 or negative. 83 | /// 84 | public HashedWheelTimer( TimeSpan tickDuration, int ticksPerWheel, long maxPendingTimeouts) 85 | { 86 | 87 | if (tickDuration == null) 88 | { 89 | throw new ArgumentNullException(nameof(tickDuration), "must be greater than 0"); 90 | } 91 | if (tickDuration.TotalMilliseconds <= 0) 92 | { 93 | throw new ArgumentOutOfRangeException(nameof(tickDuration), "must be greater than 0 ms"); 94 | } 95 | if (ticksPerWheel <= 0) 96 | { 97 | throw new ArgumentOutOfRangeException(nameof(ticksPerWheel), "must be greater than 0: "); 98 | } 99 | 100 | // Normalize ticksPerWheel to power of two and initialize the wheel. 101 | _wheel = CreateWheel(ticksPerWheel); 102 | _mask = _wheel.Length - 1; 103 | 104 | // Convert tickDuration to ms. 105 | this._tickDuration = (long)tickDuration.TotalMilliseconds; 106 | 107 | // Prevent overflow. 108 | if (this._tickDuration >= long.MaxValue / _wheel.Length) 109 | { 110 | throw new ArgumentOutOfRangeException(nameof(tickDuration) 111 | , $"{tickDuration} (expected: 0 < tickDuration in ms < {long.MaxValue / _wheel.Length}"); 112 | } 113 | _workerThread = new Thread(this.Run); 114 | 115 | this._maxPendingTimeouts = maxPendingTimeouts; 116 | } 117 | 118 | 119 | private static HashedWheelBucket[] CreateWheel(int ticksPerWheel) 120 | { 121 | if (ticksPerWheel <= 0) 122 | { 123 | throw new ArgumentOutOfRangeException(nameof(ticksPerWheel), "must be greater than 0"); 124 | } 125 | if (ticksPerWheel > 1073741824) 126 | { 127 | throw new ArgumentOutOfRangeException(nameof(ticksPerWheel), "may not be greater than 2^30"); 128 | } 129 | 130 | ticksPerWheel = NormalizeTicksPerWheel(ticksPerWheel); 131 | HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel]; 132 | for (int i = 0; i < wheel.Length; i++) 133 | { 134 | wheel[i] = new HashedWheelBucket(); 135 | } 136 | return wheel; 137 | } 138 | 139 | private static int NormalizeTicksPerWheel(int ticksPerWheel) 140 | { 141 | int normalizedTicksPerWheel = 1; 142 | while (normalizedTicksPerWheel < ticksPerWheel) 143 | { 144 | normalizedTicksPerWheel <<= 1; 145 | } 146 | return normalizedTicksPerWheel; 147 | } 148 | 149 | /// 150 | /// Starts the background thread explicitly. The background thread will start automatically on demand 151 | /// even if you did not call this method. 152 | /// 153 | private void Start() 154 | { 155 | switch (_workerState) 156 | { 157 | case WORKER_STATE_INIT: 158 | int originalWorkerState = Interlocked.CompareExchange(ref _workerState, WORKER_STATE_STARTED, WORKER_STATE_INIT); 159 | if (originalWorkerState == WORKER_STATE_INIT) 160 | { 161 | _workerThread.Start(); 162 | } 163 | break; 164 | case WORKER_STATE_STARTED: 165 | break; 166 | case WORKER_STATE_SHUTDOWN: 167 | return; 168 | default: 169 | throw new InvalidOperationException("HashedWheelTimer.workerState is invalid"); 170 | } 171 | 172 | // Wait until the startTime is initialized by the worker. 173 | while (_startTime == 0) 174 | { 175 | try 176 | { 177 | _startTimeInitialized.WaitOne(5000); 178 | } 179 | catch 180 | { 181 | // Ignore - it will be ready very soon. 182 | } 183 | } 184 | } 185 | 186 | /// 187 | /// Schedules the specified TimerTask for one-time execution after the specified delay. 188 | /// 189 | /// 190 | /// 191 | /// a handle which is associated with the specified task 192 | public Timeout NewTimeout(TimerTask task, TimeSpan span) 193 | { 194 | if (task == null) 195 | { 196 | throw new ArgumentNullException(nameof(task)); 197 | } 198 | 199 | if (_workerState == WORKER_STATE_SHUTDOWN) 200 | return null; 201 | 202 | long pendingTimeoutsCount = Interlocked.Increment(ref _pendingTimeouts); 203 | 204 | if (_maxPendingTimeouts > 0 && pendingTimeoutsCount > _maxPendingTimeouts) 205 | { 206 | Interlocked.Decrement(ref _pendingTimeouts); 207 | throw new InvalidOperationException($"Number of pending timeouts ({pendingTimeoutsCount}) is greater than or equal to maximum allowed pending timeouts ({_maxPendingTimeouts})"); 208 | } 209 | 210 | Start(); 211 | 212 | // Add the timeout to the timeout queue which will be processed on the next tick. 213 | // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket. 214 | long deadline = GetCurrentMs() + (long)span.TotalMilliseconds - _startTime; 215 | 216 | // Guard against overflow. 217 | if (span.TotalMilliseconds > 0 && deadline < 0) 218 | { 219 | deadline = long.MaxValue; 220 | } 221 | HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline); 222 | _timeouts.Enqueue(timeout); 223 | return timeout; 224 | } 225 | 226 | /// 227 | /// Releases all resources acquired by this Timer and cancels all tasks which were scheduled but not executed yet. 228 | /// 229 | /// 230 | public ISet Stop() 231 | { 232 | int originalWorkerState = Interlocked.CompareExchange(ref _workerState, WORKER_STATE_SHUTDOWN, WORKER_STATE_STARTED); 233 | 234 | if (originalWorkerState != WORKER_STATE_STARTED) 235 | { 236 | return new HashSet(); 237 | } 238 | 239 | try 240 | { 241 | while (_workerThread.IsAlive) 242 | { 243 | _workerThread.Join(1000); 244 | } 245 | } 246 | catch 247 | { 248 | 249 | } 250 | return _unprocessedTimeouts; 251 | } 252 | 253 | 254 | 255 | private readonly ISet _unprocessedTimeouts = new HashSet(); 256 | private long _tick; 257 | 258 | private void Run() 259 | { 260 | // Initialize the startTime. 261 | _startTime = GetCurrentMs(); 262 | if (_startTime == 0) 263 | { 264 | // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized. 265 | _startTime = 1; 266 | } 267 | 268 | // Notify the other threads waiting for the initialization at start(). 269 | _startTimeInitialized.Set(); 270 | 271 | do 272 | { 273 | long deadline = WaitForNextTick(); 274 | if (deadline > 0) 275 | { 276 | int idx = (int)(_tick & _mask); 277 | ProcessCancelledTasks(); 278 | HashedWheelBucket bucket = _wheel[idx]; 279 | TransferTimeoutsToBuckets(); 280 | bucket.ExpireTimeouts(deadline); 281 | _tick++; 282 | } 283 | } while (_workerState == WORKER_STATE_STARTED); 284 | 285 | // Fill the unprocessedTimeouts so we can return them from stop() method. 286 | foreach (HashedWheelBucket bucket in _wheel) 287 | { 288 | bucket.ClearTimeouts(_unprocessedTimeouts); 289 | } 290 | for (; ; ) 291 | { 292 | HashedWheelTimeout timeout; 293 | if (!_timeouts.TryDequeue(out timeout) || timeout == null) 294 | { 295 | break; 296 | } 297 | if (!timeout.Cancelled) 298 | { 299 | _unprocessedTimeouts.Add(timeout); 300 | } 301 | } 302 | ProcessCancelledTasks(); 303 | } 304 | 305 | private void TransferTimeoutsToBuckets() 306 | { 307 | // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just 308 | // adds new timeouts in a loop. 309 | for (int i = 0; i < 100000; i++) 310 | { 311 | HashedWheelTimeout timeout; 312 | if (!_timeouts.TryDequeue(out timeout) || timeout == null) 313 | { 314 | // all processed 315 | break; 316 | } 317 | if (timeout.State == HashedWheelTimeout.ST_CANCELLED) 318 | { 319 | // Was cancelled in the meantime. 320 | continue; 321 | } 322 | 323 | long calculated = timeout._deadline / _tickDuration; 324 | timeout._remainingRounds = (calculated - _tick) / _wheel.Length; 325 | 326 | long ticks = Math.Max(calculated, _tick); // Ensure we don't schedule for past. 327 | int stopIndex = (int)(ticks & _mask); 328 | 329 | HashedWheelBucket bucket = _wheel[stopIndex]; 330 | bucket.AddTimeout(timeout); 331 | } 332 | } 333 | 334 | 335 | 336 | private void ProcessCancelledTasks() 337 | { 338 | for (; ; ) 339 | { 340 | HashedWheelTimeout timeout; 341 | if (!_cancelledTimeouts.TryDequeue(out timeout) || timeout == null) 342 | { 343 | // all processed 344 | break; 345 | } 346 | try 347 | { 348 | timeout.Remove(); 349 | } 350 | catch (Exception t) 351 | { 352 | /* 353 | if (logger.isWarnEnabled()) 354 | { 355 | logger.warn("An exception was thrown while process a cancellation task", t); 356 | } 357 | */ 358 | } 359 | } 360 | } 361 | 362 | 363 | private long WaitForNextTick() 364 | { 365 | long deadline = _tickDuration * (_tick + 1); 366 | 367 | for (; ; ) 368 | { 369 | long currentTime = GetCurrentMs() - _startTime; 370 | int sleepTimeMs = (int)Math.Truncate(deadline - currentTime + 1M); 371 | 372 | if (sleepTimeMs <= 0) 373 | { 374 | if (currentTime == long.MaxValue) 375 | { 376 | return -long.MaxValue; 377 | } 378 | else 379 | { 380 | return currentTime; 381 | } 382 | } 383 | 384 | Thread.Sleep(sleepTimeMs); 385 | } 386 | } 387 | 388 | public TimedAwaiter Delay(long milliseconds) 389 | { 390 | TimedAwaiter awaiter = new TimedAwaiter(); 391 | this.NewTimeout(awaiter, TimeSpan.FromMilliseconds(milliseconds)); 392 | return awaiter; 393 | } 394 | 395 | 396 | 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /HWT/TimedAwaiter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace HWT 6 | { 7 | public sealed class TimedAwaiter : INotifyCompletion, TimerTask 8 | { 9 | public bool IsCompleted { get; private set; } 10 | 11 | 12 | private Action _continuation; 13 | 14 | public void OnCompleted(Action continuation) 15 | { 16 | _continuation = continuation; 17 | if (IsCompleted) 18 | Interlocked.Exchange(ref _continuation, null)?.Invoke(); 19 | } 20 | 21 | public void Run(Timeout timeout) 22 | { 23 | IsCompleted = true; 24 | Interlocked.Exchange(ref _continuation, null)?.Invoke(); 25 | } 26 | 27 | public TimedAwaiter GetAwaiter() 28 | { 29 | return this; 30 | } 31 | 32 | public object GetResult() 33 | { 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /HWT/Timeout.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HWT 3 | { 4 | /// 5 | /// A handle associated with a TimerTask that is returned by a Timer 6 | /// 7 | public interface Timeout 8 | { 9 | /// 10 | /// Returns the Timer that created this handle. 11 | /// 12 | Timer Timer { get; } 13 | 14 | /// 15 | /// Returns the TimerTask which is associated with this handle. 16 | /// 17 | TimerTask TimerTask { get; } 18 | 19 | /// 20 | /// Returns true if and only if the TimerTask associated 21 | /// with this handle has been expired 22 | /// 23 | bool Expired { get; } 24 | 25 | /// 26 | /// Returns true if and only if the TimerTask associated 27 | /// with this handle has been cancelled 28 | /// 29 | bool Cancelled { get; } 30 | 31 | /// 32 | /// Attempts to cancel the {@link TimerTask} associated with this handle. 33 | /// If the task has been executed or cancelled already, it will return with no side effect. 34 | /// 35 | /// True if the cancellation completed successfully, otherwise false 36 | bool Cancel(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /HWT/Timer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | 5 | namespace HWT 6 | { 7 | public interface Timer 8 | { 9 | /// 10 | /// Schedules the specified TimerTask for one-time execution after the specified delay. 11 | /// 12 | /// 13 | /// 14 | /// handle which is associated with the specified task 15 | Timeout NewTimeout(TimerTask task, TimeSpan span); 16 | 17 | 18 | /// 19 | /// Releases all resources acquired by this Timer and cancels all 20 | /// tasks which were scheduled but not executed yet. 21 | /// 22 | /// the handles associated with the tasks which were canceled by this method 23 | ISet Stop(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /HWT/TimerTask.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HWT 3 | { 4 | /// 5 | /// A task which is executed after the delay specified with Timer.NewTimeout(TimerTask, long, TimeUnit). 6 | /// 7 | public interface TimerTask 8 | { 9 | /// 10 | /// Executed after the delay specified with Timer.NewTimeout(TimerTask, long, TimeUnit) 11 | /// 12 | /// timeout a handle which is associated with this task 13 | void Run(Timeout timeout); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /HashWheelTimerApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.10 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HashWheelTimerApp", "HashWheelTimerApp\HashWheelTimerApp.csproj", "{A3DE9FDB-5EBB-4E29-A6DC-FA1545649F9C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HWT", "HWT\HWT.csproj", "{7B088B4D-A9CB-403C-BC18-88FB2C4939CC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A3DE9FDB-5EBB-4E29-A6DC-FA1545649F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {A3DE9FDB-5EBB-4E29-A6DC-FA1545649F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {A3DE9FDB-5EBB-4E29-A6DC-FA1545649F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {A3DE9FDB-5EBB-4E29-A6DC-FA1545649F9C}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7B088B4D-A9CB-403C-BC18-88FB2C4939CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7B088B4D-A9CB-403C-BC18-88FB2C4939CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7B088B4D-A9CB-403C-BC18-88FB2C4939CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7B088B4D-A9CB-403C-BC18-88FB2C4939CC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {AF33D7FA-08A5-4683-8577-64AC99A0A8E8} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /HashWheelTimerApp/HashWheelTimerApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /HashWheelTimerApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using HWT; 4 | namespace HashWheelTimerApp 5 | { 6 | 7 | class Program 8 | { 9 | 10 | 11 | /// 12 | /// Task fired repeatedly 13 | /// 14 | class IntervalTimerTask : TimerTask 15 | { 16 | public void Run(Timeout timeout) 17 | { 18 | Console.WriteLine($"IntervalTimerTask is fired at {DateTime.UtcNow.Ticks / 10000000L}"); 19 | timeout.Timer.NewTimeout(this, TimeSpan.FromSeconds(2)); 20 | } 21 | } 22 | 23 | /// 24 | /// Task only be fired for one time 25 | /// 26 | class OneTimeTask : TimerTask 27 | { 28 | readonly string _userData; 29 | public OneTimeTask(string data) 30 | { 31 | _userData = data; 32 | } 33 | 34 | public void Run(Timeout timeout) 35 | { 36 | Console.WriteLine($"{_userData} is fired at {DateTime.UtcNow.Ticks / 10000000L}"); 37 | } 38 | } 39 | 40 | 41 | static void Main(string[] args) 42 | { 43 | Console.WriteLine($"{DateTime.UtcNow.Ticks / 10000000L} : Started"); 44 | 45 | HashedWheelTimer timer = new HashedWheelTimer( tickDuration: TimeSpan.FromMilliseconds(100) 46 | , ticksPerWheel: 100000 47 | , maxPendingTimeouts: 0); 48 | 49 | timer.NewTimeout(new OneTimeTask("A"), TimeSpan.FromSeconds(5)); 50 | timer.NewTimeout(new OneTimeTask("B"), TimeSpan.FromSeconds(4)); 51 | var timeout = timer.NewTimeout(new OneTimeTask("C"), TimeSpan.FromSeconds(3)); 52 | timer.NewTimeout(new OneTimeTask("D"), TimeSpan.FromSeconds(2)); 53 | timer.NewTimeout(new OneTimeTask("E"), TimeSpan.FromSeconds(1)); 54 | 55 | timeout.Cancel(); 56 | 57 | timer.NewTimeout(new IntervalTimerTask(), TimeSpan.FromSeconds(5)); 58 | 59 | 60 | System.Threading.Thread.Sleep(7000); 61 | TestDelay(timer); 62 | 63 | Console.ReadKey(); 64 | timer.Stop(); 65 | Console.WriteLine($"{DateTime.UtcNow.Ticks / 10000000L} : Stopped"); 66 | Console.ReadKey(); 67 | } 68 | 69 | static async void TestDelay(HashedWheelTimer timer) 70 | { 71 | Console.WriteLine($"Async Delay starts at {DateTime.UtcNow.Ticks / 10000000L}"); 72 | await timer.Delay(1000); 73 | Console.WriteLine($"Async Delay ends at {DateTime.UtcNow.Ticks / 10000000L}"); 74 | } 75 | 76 | 77 | 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jerry Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # High Performance Timer for .NET 2 | HashedWheelTimer implemented in C# inspired by io.netty.util.HashedWheelTimer 3 | 4 | ## What is Hashed Wheel Timer? 5 | 6 | It is a timer based on George Varghese and Tony Lauck's paper, [Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility](http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z). 7 | 8 | ![](./hwt.png) 9 | 10 | The concept on the Timer Wheel is rather simple to understand: in order to keep 11 | track of events on given resolution, an array of linked lists (alternatively - 12 | sets or even arrays, YMMV) is preallocated. When event is scheduled, it's 13 | address is found by dividing deadline time `t` by `resolution` and `wheel size`. 14 | The registration is then assigned with `rounds` (how many times we should go 15 | around the wheel in order for the time period to be elapsed). 16 | 17 | For each scheduled resolution, a __bucket__ is created. There are `wheel size` 18 | buckets, each one of which is holding `Registrations`. Timer is going through 19 | each `bucket` from the first until the next one, and decrements `rounds` for 20 | each registration. As soon as registration's `rounds` is reaching 0, the timeout 21 | is triggered. After that it is either rescheduled (with same `offset` and amount 22 | of `rounds` as initially) or removed from timer. 23 | 24 | Hashed Wheel is often called __approximated timer__, since it acts on the 25 | certain resolution, which allows it's optimisations. All the tasks scheduled for 26 | the timer period lower than the resolution or "between" resolution steps will be 27 | rounded to the "ceiling" (for example, given resolution 10 milliseconds, all the 28 | tasks for 5,6,7 etc milliseconds will first fire after 10, and 15, 16, 17 will 29 | first trigger after 20). 30 | 31 | If you're a visual person, it might be useful for you to check out [these 32 | slides](http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt), 33 | which help to understand the concept underlying the Hashed Wheel Timer better. 34 | 35 | ## Why another Timer? 36 | 37 | The .NET Framework and .NET Core already provide a set of timers 38 | 39 | * `System.Timers.Timer` 40 | * `System.Threading.Timer` 41 | * `System.Windows.Forms.Timer` 42 | * `System.Web.UI.Timer` 43 | * `System.Windows.Threading.DispatcherTimer` 44 | 45 | HashedWheelTimer is different as it is optimized for approximated I/O timeout scheduling. It provides __O(1) time complexity__ and cheap constant factors for the important operations of inserting or removing timers. 46 | 47 | Imagine a scenario, there are ten thousands of TCP connections and each of them has its own timer for rate limit or other functionalities. In this case HashedWheelTimer is a better choice. 48 | 49 | 50 | 51 | 52 | ## Installation 53 | 54 | The package is available from NuGet 55 | ``` 56 | Install-Package HashedWheelTimer 57 | ``` 58 | 59 | 60 | ## How to Use 61 | 62 | First create an instance of `HashedWheelTimer` class. 63 | 64 | Note that, each `HashedWheelTimer` instance creates a dedicated thread to watch the wheel. Hence, it is __strong recommanded__ to reuse `HashedWheelTimer` instance as much as possible. 65 | ```csharp 66 | using HWT; 67 | 68 | HashedWheelTimer timer = new HashedWheelTimer( tickDuration: TimeSpan.FromSeconds(1) 69 | , ticksPerWheel: 100000 70 | , maxPendingTimeouts: 0); 71 | ``` 72 | 73 | The constructor accepts the parameters below. 74 | 75 | * `tickDuration` As described with 'approximated', this timer does not execute the scheduled TimerTask on time. HashedWheelTimer, on every tick, will check if there are any TimerTasks behind the schedule and execute them. You can increase or decrease the accuracy of the execution timing by specifying smaller or larger tick duration in the constructor. In most network applications, I/O timeout does not need to be accurate. 76 | * `ticksPerWheel` HashedWheelTimer maintains a data structure called 'wheel'. To put simply, a wheel is a hash table of TimerTasks whose hash function is 'dead line of the task'. The default number of ticks per wheel (i.e. the size of the wheel) is 512. You could specify a larger value if you are going to schedule a lot of timeouts. 77 | * `maxPendingTimeouts` The maximum number of pending timeouts after which call to _NewTimeout()_ will result in InvalidOperationException being thrown. No maximum pending timeouts limit is assumed if this value is 0 or negative. 78 | 79 | 80 | Next, create a new class implementing `TimerTask` interface 81 | 82 | ```csharp 83 | class MyTimerTask : TimerTask 84 | { 85 | // This method is called when the task is expired. 86 | // It is fired via `Task.Run`, hence you'd better surround the code with try-catch 87 | public void Run(Timeout timeout) 88 | { 89 | try 90 | { 91 | // ... 92 | } catch(Exception){ 93 | // ... 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | Now we can schedual the task in 5 seconds later. 100 | ```csharp 101 | timer.NewTimeout(new MyTimerTask(), TimeSpan.FromSeconds(5)); 102 | ``` 103 | 104 | Note that all the methods are __thread-safe__. You don't need synchronization on accessing them. 105 | 106 | 107 | If you are using TPL(Task Parallel Library) asynchronous programming, you may already use `await Task.Delay(milliseconds)` to continue some work after a while. Alternatively, it can be replaced with following code if approximated delay is acceptable. 108 | 109 | ```csharp 110 | await timer.Delay(milliseconds) 111 | ``` 112 | 113 | ## A full example 114 | 115 | ```csharp 116 | /// 117 | /// Task fired repeatedly 118 | /// 119 | class IntervalTimerTask : TimerTask 120 | { 121 | public void Run(Timeout timeout) 122 | { 123 | Console.WriteLine($"IntervalTimerTask is fired at {DateTime.UtcNow.Ticks / 10000000L}"); 124 | timeout.Timer.NewTimeout(this, TimeSpan.FromSeconds(2)); 125 | } 126 | } 127 | 128 | /// 129 | /// Task only be fired for one time 130 | /// 131 | class OneTimeTask : TimerTask 132 | { 133 | readonly string _userData; 134 | public OneTimeTask(string data) 135 | { 136 | _userData = data; 137 | } 138 | 139 | public void Run(Timeout timeout) 140 | { 141 | Console.WriteLine($"{_userData} is fired at {DateTime.UtcNow.Ticks / 10000000L}"); 142 | } 143 | } 144 | 145 | 146 | static void Main(string[] args) 147 | { 148 | HashedWheelTimer timer = new HashedWheelTimer( tickDuration: TimeSpan.FromSeconds(1) 149 | , ticksPerWheel: 100000 150 | , maxPendingTimeouts: 0); 151 | 152 | timer.NewTimeout(new OneTimeTask("A"), TimeSpan.FromSeconds(5)); 153 | timer.NewTimeout(new OneTimeTask("B"), TimeSpan.FromSeconds(4)); 154 | var timeout = timer.NewTimeout(new OneTimeTask("C"), TimeSpan.FromSeconds(3)); 155 | timer.NewTimeout(new OneTimeTask("D"), TimeSpan.FromSeconds(2)); 156 | timer.NewTimeout(new OneTimeTask("E"), TimeSpan.FromSeconds(1)); 157 | 158 | timeout.Cancel(); 159 | 160 | timer.NewTimeout(new IntervalTimerTask(), TimeSpan.FromSeconds(5)); 161 | Console.WriteLine($"{DateTime.UtcNow.Ticks / 10000000L} : Started"); 162 | Console.ReadKey(); 163 | } 164 | ``` 165 | 166 | The output of the sample is something like 167 | ![](./console.png) 168 | -------------------------------------------------------------------------------- /console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/HashedWheelTimer/a67c18d27a0648c98bcd7dba43cbedad4aea8c92/console.png -------------------------------------------------------------------------------- /hwt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/HashedWheelTimer/a67c18d27a0648c98bcd7dba43cbedad4aea8c92/hwt.png --------------------------------------------------------------------------------