1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections;
6 using System.Collections.Generic;
7 using System.Runtime.InteropServices;
8 using System.Threading;
9 
10 namespace System.Net
11 {
12     /// <summary>
13     /// <para>Acts as countdown timer, used to measure elapsed time over a sync operation.</para>
14     /// </summary>
15     internal static class TimerThread
16     {
17         /// <summary>
18         /// <para>Represents a queue of timers, which all have the same duration.</para>
19         /// </summary>
20         internal abstract class Queue
21         {
22             private readonly int _durationMilliseconds;
23 
Queue(int durationMilliseconds)24             internal Queue(int durationMilliseconds)
25             {
26                 _durationMilliseconds = durationMilliseconds;
27             }
28 
29             /// <summary>
30             /// <para>The duration in milliseconds of timers in this queue.</para>
31             /// </summary>
32             internal int Duration => _durationMilliseconds;
33 
34             /// <summary>
35             /// <para>Creates and returns a handle to a new timer with attached context.</para>
36             /// </summary>
CreateTimer(Callback callback, object context)37             internal abstract Timer CreateTimer(Callback callback, object context);
38         }
39 
40         /// <summary>
41         /// <para>Represents a timer and provides a mechanism to cancel.</para>
42         /// </summary>
43         internal abstract class Timer : IDisposable
44         {
45             private readonly int _startTimeMilliseconds;
46             private readonly int _durationMilliseconds;
47 
Timer(int durationMilliseconds)48             internal Timer(int durationMilliseconds)
49             {
50                 _durationMilliseconds = durationMilliseconds;
51                 _startTimeMilliseconds = Environment.TickCount;
52             }
53 
54             /// <summary>
55             /// <para>The time (relative to Environment.TickCount) when the timer started.</para>
56             /// </summary>
57             internal int StartTime => _startTimeMilliseconds;
58 
59             /// <summary>
60             /// <para>The time (relative to Environment.TickCount) when the timer will expire.</para>
61             /// </summary>
62             internal int Expiration => unchecked(_startTimeMilliseconds + _durationMilliseconds);
63 
64             /// <summary>
65             /// <para>Cancels the timer.  Returns true if the timer hasn't and won't fire; false if it has or will.</para>
66             /// </summary>
Cancel()67             internal abstract bool Cancel();
68 
69             /// <summary>
70             /// <para>Whether or not the timer has expired.</para>
71             /// </summary>
72             internal abstract bool HasExpired { get; }
73 
Dispose()74             public void Dispose() => Cancel();
75         }
76 
77         /// <summary>
78         /// <para>Prototype for the callback that is called when a timer expires.</para>
79         /// </summary>
Callback(Timer timer, int timeNoticed, object context)80         internal delegate void Callback(Timer timer, int timeNoticed, object context);
81 
82         private const int ThreadIdleTimeoutMilliseconds = 30 * 1000;
83         private const int CacheScanPerIterations = 32;
84         private const int TickCountResolution = 15;
85 
86         private static readonly LinkedList<WeakReference> s_queues = new LinkedList<WeakReference>();
87         private static readonly LinkedList<WeakReference> s_newQueues = new LinkedList<WeakReference>();
88         private static int s_threadState = (int)TimerThreadState.Idle;  // Really a TimerThreadState, but need an int for Interlocked.
89         private static readonly AutoResetEvent s_threadReadyEvent = new AutoResetEvent(false);
90         private static readonly ManualResetEvent s_threadShutdownEvent = new ManualResetEvent(false);
91         private static readonly WaitHandle[] s_threadEvents = { s_threadShutdownEvent, s_threadReadyEvent };
92         private static int s_cacheScanIteration;
93         private static readonly Hashtable s_queuesCache = new Hashtable();
94 
95         /// <summary>
96         /// <para>The possible states of the timer thread.</para>
97         /// </summary>
98         private enum TimerThreadState
99         {
100             Idle,
101             Running,
102             Stopped
103         }
104 
105         /// <summary>
106         /// <para>Queue factory.  Always synchronized.</para>
107         /// </summary>
GetOrCreateQueue(int durationMilliseconds)108         internal static Queue GetOrCreateQueue(int durationMilliseconds)
109         {
110             if (durationMilliseconds == Timeout.Infinite)
111             {
112                 return new InfiniteTimerQueue();
113             }
114 
115             if (durationMilliseconds < 0)
116             {
117                 throw new ArgumentOutOfRangeException(nameof(durationMilliseconds));
118             }
119 
120             TimerQueue queue;
121             object key = durationMilliseconds; // Box once.
122             WeakReference weakQueue = (WeakReference)s_queuesCache[key];
123             if (weakQueue == null || (queue = (TimerQueue)weakQueue.Target) == null)
124             {
125                 lock (s_newQueues)
126                 {
127                     weakQueue = (WeakReference)s_queuesCache[key];
128                     if (weakQueue == null || (queue = (TimerQueue)weakQueue.Target) == null)
129                     {
130                         queue = new TimerQueue(durationMilliseconds);
131                         weakQueue = new WeakReference(queue);
132                         s_newQueues.AddLast(weakQueue);
133                         s_queuesCache[key] = weakQueue;
134 
135                         // Take advantage of this lock to periodically scan the table for garbage.
136                         if (++s_cacheScanIteration % CacheScanPerIterations == 0)
137                         {
138                             var garbage = new List<object>();
139                             // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
140                             IDictionaryEnumerator e = s_queuesCache.GetEnumerator();
141                             while (e.MoveNext())
142                             {
143                                 DictionaryEntry pair = e.Entry;
144                                 if (((WeakReference)pair.Value).Target == null)
145                                 {
146                                     garbage.Add(pair.Key);
147                                 }
148                             }
149                             for (int i = 0; i < garbage.Count; i++)
150                             {
151                                 s_queuesCache.Remove(garbage[i]);
152                             }
153                         }
154                     }
155                 }
156             }
157 
158             return queue;
159         }
160 
161         /// <summary>
162         /// <para>Represents a queue of timers of fixed duration.</para>
163         /// </summary>
164         private class TimerQueue : Queue
165         {
166             // This is a GCHandle that holds onto the TimerQueue when active timers are in it.
167             // The TimerThread only holds WeakReferences to it so that it can be collected when the user lets go of it.
168             // But we don't want the user to HAVE to keep a reference to it when timers are active in it.
169             // It gets created when the first timer gets added, and cleaned up when the TimerThread notices it's empty.
170             // The TimerThread will always notice it's empty eventually, since the TimerThread will always wake up and
171             // try to fire the timer, even if it was cancelled and removed prematurely.
172             private IntPtr _thisHandle;
173 
174             // This sentinel TimerNode acts as both the head and the tail, allowing nodes to go in and out of the list without updating
175             // any TimerQueue members.  _timers.Next is the true head, and .Prev the true tail.  This also serves as the list's lock.
176             private readonly TimerNode _timers;
177 
178             /// <summary>
179             /// <para>Create a new TimerQueue.  TimerQueues must be created while s_NewQueues is locked in
180             /// order to synchronize with Shutdown().</para>
181             /// </summary>
182             /// <param name="durationMilliseconds"></param>
TimerQueue(int durationMilliseconds)183             internal TimerQueue(int durationMilliseconds) :
184                 base(durationMilliseconds)
185             {
186                 // Create the doubly-linked list with a sentinel head and tail so that this member never needs updating.
187                 _timers = new TimerNode();
188                 _timers.Next = _timers;
189                 _timers.Prev = _timers;
190             }
191 
192             /// <summary>
193             /// <para>Creates new timers.  This method is thread-safe.</para>
194             /// </summary>
CreateTimer(Callback callback, object context)195             internal override Timer CreateTimer(Callback callback, object context)
196             {
197                 TimerNode timer = new TimerNode(callback, context, Duration, _timers);
198 
199                 // Add this on the tail.  (Actually, one before the tail - _timers is the sentinel tail.)
200                 bool needProd = false;
201                 lock (_timers)
202                 {
203                     if (!(_timers.Prev.Next == _timers))
204                     {
205                         NetEventSource.Fail(this, $"Tail corruption.");
206                     }
207 
208                     // If this is the first timer in the list, we need to create a queue handle and prod the timer thread.
209                     if (_timers.Next == _timers)
210                     {
211                         if (_thisHandle == IntPtr.Zero)
212                         {
213                             _thisHandle = (IntPtr)GCHandle.Alloc(this);
214                         }
215                         needProd = true;
216                     }
217 
218                     timer.Next = _timers;
219                     timer.Prev = _timers.Prev;
220                     _timers.Prev.Next = timer;
221                     _timers.Prev = timer;
222                 }
223 
224                 // If, after we add the new tail, there is a chance that the tail is the next
225                 // node to be processed, we need to wake up the timer thread.
226                 if (needProd)
227                 {
228                     TimerThread.Prod();
229                 }
230 
231                 return timer;
232             }
233 
234             /// <summary>
235             /// <para>Called by the timer thread to fire the expired timers.  Returns true if there are future timers
236             /// in the queue, and if so, also sets nextExpiration.</para>
237             /// </summary>
Fire(out int nextExpiration)238             internal bool Fire(out int nextExpiration)
239             {
240                 while (true)
241                 {
242                     // Check if we got to the end.  If so, free the handle.
243                     TimerNode timer = _timers.Next;
244                     if (timer == _timers)
245                     {
246                         lock (_timers)
247                         {
248                             timer = _timers.Next;
249                             if (timer == _timers)
250                             {
251                                 if (_thisHandle != IntPtr.Zero)
252                                 {
253                                     ((GCHandle)_thisHandle).Free();
254                                     _thisHandle = IntPtr.Zero;
255                                 }
256 
257                                 nextExpiration = 0;
258                                 return false;
259                             }
260                         }
261                     }
262 
263                     if (!timer.Fire())
264                     {
265                         nextExpiration = timer.Expiration;
266                         return true;
267                     }
268                 }
269             }
270         }
271 
272         /// <summary>
273         /// <para>A special dummy implementation for a queue of timers of infinite duration.</para>
274         /// </summary>
275         private class InfiniteTimerQueue : Queue
276         {
InfiniteTimerQueue()277             internal InfiniteTimerQueue() : base(Timeout.Infinite) { }
278 
279             /// <summary>
280             /// <para>Always returns a dummy infinite timer.</para>
281             /// </summary>
CreateTimer(Callback callback, object context)282             internal override Timer CreateTimer(Callback callback, object context) => new InfiniteTimer();
283         }
284 
285         /// <summary>
286         /// <para>Internal representation of an individual timer.</para>
287         /// </summary>
288         private class TimerNode : Timer
289         {
290             private TimerState _timerState;
291             private Callback _callback;
292             private object _context;
293             private object _queueLock;
294             private TimerNode _next;
295             private TimerNode _prev;
296 
297             /// <summary>
298             /// <para>Status of the timer.</para>
299             /// </summary>
300             private enum TimerState
301             {
302                 Ready,
303                 Fired,
304                 Cancelled,
305                 Sentinel
306             }
307 
TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock)308             internal TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock) : base(durationMilliseconds)
309             {
310                 if (callback != null)
311                 {
312                     _callback = callback;
313                     _context = context;
314                 }
315                 _timerState = TimerState.Ready;
316                 _queueLock = queueLock;
317                 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}");
318             }
319 
320             // A sentinel node - both the head and tail are one, which prevent the head and tail from ever having to be updated.
TimerNode()321             internal TimerNode() : base(0)
322             {
323                 _timerState = TimerState.Sentinel;
324             }
325 
326             internal override bool HasExpired => _timerState == TimerState.Fired;
327 
328             internal TimerNode Next
329             {
330                 get { return _next; }
331                 set { _next = value; }
332             }
333 
334             internal TimerNode Prev
335             {
336                 get { return _prev; }
337                 set { _prev = value; }
338             }
339 
340             /// <summary>
341             /// <para>Cancels the timer.  Returns true if it hasn't and won't fire; false if it has or will, or has already been cancelled.</para>
342             /// </summary>
Cancel()343             internal override bool Cancel()
344             {
345                 if (_timerState == TimerState.Ready)
346                 {
347                     lock (_queueLock)
348                     {
349                         if (_timerState == TimerState.Ready)
350                         {
351                             // Remove it from the list.  This keeps the list from getting too big when there are a lot of rapid creations
352                             // and cancellations.  This is done before setting it to Cancelled to try to prevent the Fire() loop from
353                             // seeing it, or if it does, of having to take a lock to synchronize with the state of the list.
354                             Next.Prev = Prev;
355                             Prev.Next = Next;
356 
357                             // Just cleanup.  Doesn't need to be in the lock but is easier to have here.
358                             Next = null;
359                             Prev = null;
360                             _callback = null;
361                             _context = null;
362 
363                             _timerState = TimerState.Cancelled;
364 
365                             if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (success)");
366                             return true;
367                         }
368                     }
369                 }
370 
371                 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (failure)");
372                 return false;
373             }
374 
375             /// <summary>
376             /// <para>Fires the timer if it is still active and has expired.  Returns
377             /// true if it can be deleted, or false if it is still timing.</para>
378             /// </summary>
Fire()379             internal bool Fire()
380             {
381                 if (_timerState == TimerState.Sentinel)
382                 {
383                     if (NetEventSource.IsEnabled) NetEventSource.Info(this, "TimerQueue tried to Fire a Sentinel.");
384                 }
385 
386                 if (_timerState != TimerState.Ready)
387                 {
388                     return true;
389                 }
390 
391                 // Must get the current tick count within this method so it is guaranteed not to be before
392                 // StartTime, which is set in the constructor.
393                 int nowMilliseconds = Environment.TickCount;
394                 if (IsTickBetween(StartTime, Expiration, nowMilliseconds))
395                 {
396                     if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Not firing ({StartTime} <= {nowMilliseconds} < {Expiration})");
397                     return false;
398                 }
399 
400                 bool needCallback = false;
401                 lock (_queueLock)
402                 {
403                     if (_timerState == TimerState.Ready)
404                     {
405                         if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Firing ({StartTime} <= {nowMilliseconds} >= " + Expiration + ")");
406                         _timerState = TimerState.Fired;
407 
408                         // Remove it from the list.
409                         Next.Prev = Prev;
410                         Prev.Next = Next;
411 
412                         Next = null;
413                         Prev = null;
414                         needCallback = _callback != null;
415                     }
416                 }
417 
418                 if (needCallback)
419                 {
420                     try
421                     {
422                         Callback callback = _callback;
423                         object context = _context;
424                         _callback = null;
425                         _context = null;
426                         callback(this, nowMilliseconds, context);
427                     }
428                     catch (Exception exception)
429                     {
430                         if (ExceptionCheck.IsFatal(exception))
431                             throw;
432 
433                         if (NetEventSource.IsEnabled) NetEventSource.Error(this, $"exception in callback: {exception}");
434 
435                         // This thread is not allowed to go into user code, so we should never get an exception here.
436                         // So, in debug, throw it up, killing the AppDomain.  In release, we'll just ignore it.
437 #if DEBUG
438                         throw;
439 #endif
440                     }
441                 }
442 
443                 return true;
444             }
445         }
446 
447         /// <summary>
448         /// <para>A dummy infinite timer.</para>
449         /// </summary>
450         private class InfiniteTimer : Timer
451         {
InfiniteTimer()452             internal InfiniteTimer() : base(Timeout.Infinite) { }
453 
454             private int _cancelled;
455 
456             internal override bool HasExpired => false;
457 
458             /// <summary>
459             /// <para>Cancels the timer.  Returns true the first time, false after that.</para>
460             /// </summary>
Cancel()461             internal override bool Cancel() => Interlocked.Exchange(ref _cancelled, 1) == 0;
462         }
463 
464         /// <summary>
465         /// <para>Internal mechanism used when timers are added to wake up / create the thread.</para>
466         /// </summary>
Prod()467         private static void Prod()
468         {
469             s_threadReadyEvent.Set();
470             TimerThreadState oldState = (TimerThreadState)Interlocked.CompareExchange(
471                 ref s_threadState,
472                 (int)TimerThreadState.Running,
473                 (int)TimerThreadState.Idle);
474 
475             if (oldState == TimerThreadState.Idle)
476             {
477                 new Thread(new ThreadStart(ThreadProc)).Start();
478             }
479         }
480 
481         /// <summary>
482         /// <para>Thread for the timer.  Ignores all exceptions.  If no activity occurs for a while,
483         /// the thread will shut down.</para>
484         /// </summary>
ThreadProc()485         private static void ThreadProc()
486         {
487             if (NetEventSource.IsEnabled) NetEventSource.Enter(null);
488 #if DEBUG
489             DebugThreadTracking.SetThreadSource(ThreadKinds.Timer);
490             using (DebugThreadTracking.SetThreadKind(ThreadKinds.System | ThreadKinds.Async))
491             {
492 #endif
493                 // Set this thread as a background thread.  On AppDomain/Process shutdown, the thread will just be killed.
494                 Thread.CurrentThread.IsBackground = true;
495 
496                 // Keep a permanent lock on s_Queues.  This lets for example Shutdown() know when this thread isn't running.
497                 lock (s_queues)
498                 {
499                     // If shutdown was recently called, abort here.
500                     if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Running) !=
501                         (int)TimerThreadState.Running)
502                     {
503                         return;
504                     }
505 
506                     bool running = true;
507                     while (running)
508                     {
509                         try
510                         {
511                             s_threadReadyEvent.Reset();
512 
513                             while (true)
514                             {
515                                 // Copy all the new queues to the real queues.  Since only this thread modifies the real queues, it doesn't have to lock it.
516                                 if (s_newQueues.Count > 0)
517                                 {
518                                     lock (s_newQueues)
519                                     {
520                                         for (LinkedListNode<WeakReference> node = s_newQueues.First; node != null; node = s_newQueues.First)
521                                         {
522                                             s_newQueues.Remove(node);
523                                             s_queues.AddLast(node);
524                                         }
525                                     }
526                                 }
527 
528                                 int now = Environment.TickCount;
529                                 int nextTick = 0;
530                                 bool haveNextTick = false;
531                                 for (LinkedListNode<WeakReference> node = s_queues.First; node != null; /* node = node.Next must be done in the body */)
532                                 {
533                                     TimerQueue queue = (TimerQueue)node.Value.Target;
534                                     if (queue == null)
535                                     {
536                                         LinkedListNode<WeakReference> next = node.Next;
537                                         s_queues.Remove(node);
538                                         node = next;
539                                         continue;
540                                     }
541 
542                                     // Fire() will always return values that should be interpreted as later than 'now' (that is, even if 'now' is
543                                     // returned, it is 0x100000000 milliseconds in the future).  There's also a chance that Fire() will return a value
544                                     // intended as > 0x100000000 milliseconds from 'now'.  Either case will just cause an extra scan through the timers.
545                                     int nextTickInstance;
546                                     if (queue.Fire(out nextTickInstance) && (!haveNextTick || IsTickBetween(now, nextTick, nextTickInstance)))
547                                     {
548                                         nextTick = nextTickInstance;
549                                         haveNextTick = true;
550                                     }
551 
552                                     node = node.Next;
553                                 }
554 
555                                 // Figure out how long to wait, taking into account how long the loop took.
556                                 // Add 15 ms to compensate for poor TickCount resolution (want to guarantee a firing).
557                                 int newNow = Environment.TickCount;
558                                 int waitDuration = haveNextTick ?
559                                     (int)(IsTickBetween(now, nextTick, newNow) ?
560                                         Math.Min(unchecked((uint)(nextTick - newNow)), (uint)(Int32.MaxValue - TickCountResolution)) + TickCountResolution :
561                                         0) :
562                                     ThreadIdleTimeoutMilliseconds;
563 
564                                 if (NetEventSource.IsEnabled) NetEventSource.Info(null, $"Waiting for {waitDuration}ms");
565 
566                                 int waitResult = WaitHandle.WaitAny(s_threadEvents, waitDuration, false);
567 
568                                 // 0 is s_ThreadShutdownEvent - die.
569                                 if (waitResult == 0)
570                                 {
571                                     if (NetEventSource.IsEnabled) NetEventSource.Info(null, "Awoke, cause: Shutdown");
572                                     running = false;
573                                     break;
574                                 }
575 
576                                 if (NetEventSource.IsEnabled) NetEventSource.Info(null, $"Awoke, cause {(waitResult == WaitHandle.WaitTimeout ? "Timeout" : "Prod")}");
577 
578                                 // If we timed out with nothing to do, shut down.
579                                 if (waitResult == WaitHandle.WaitTimeout && !haveNextTick)
580                                 {
581                                     Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Idle, (int)TimerThreadState.Running);
582                                     // There could have been one more prod between the wait and the exchange.  Check, and abort if necessary.
583                                     if (s_threadReadyEvent.WaitOne(0, false))
584                                     {
585                                         if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Idle) ==
586                                             (int)TimerThreadState.Idle)
587                                         {
588                                             continue;
589                                         }
590                                     }
591 
592                                     running = false;
593                                     break;
594                                 }
595                             }
596                         }
597                         catch (Exception exception)
598                         {
599                             if (ExceptionCheck.IsFatal(exception))
600                                 throw;
601 
602                             if (NetEventSource.IsEnabled) NetEventSource.Error(null, exception);
603 
604                             // The only options are to continue processing and likely enter an error-loop,
605                             // shut down timers for this AppDomain, or shut down the AppDomain.  Go with shutting
606                             // down the AppDomain in debug, and going into a loop in retail, but try to make the
607                             // loop somewhat slow.  Note that in retail, this can only be triggered by OutOfMemory or StackOverflow,
608                             // or an exception thrown within TimerThread - the rest are caught in Fire().
609 #if !DEBUG
610                             Thread.Sleep(1000);
611 #else
612                             throw;
613 #endif
614                         }
615                     }
616                 }
617 
618                 if (NetEventSource.IsEnabled) NetEventSource.Info(null, "Stop");
619 #if DEBUG
620             }
621 #endif
622         }
623 
624         /// <summary>
625         /// <para>Helper for deciding whether a given TickCount is before or after a given expiration
626         /// tick count assuming that it can't be before a given starting TickCount.</para>
627         /// </summary>
IsTickBetween(int start, int end, int comparand)628         private static bool IsTickBetween(int start, int end, int comparand)
629         {
630             // Assumes that if start and end are equal, they are the same time.
631             // Assumes that if the comparand and start are equal, no time has passed,
632             // and that if the comparand and end are equal, end has occurred.
633             return ((start <= comparand) == (end <= comparand)) != (start <= end);
634         }
635     }
636 }
637