├── .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 | 
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 | 
168 |
--------------------------------------------------------------------------------
/console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangjia184/HashedWheelTimer/a67c18d27a0648c98bcd7dba43cbedad4aea8c92/console.png
--------------------------------------------------------------------------------
/hwt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangjia184/HashedWheelTimer/a67c18d27a0648c98bcd7dba43cbedad4aea8c92/hwt.png
--------------------------------------------------------------------------------