1 //------------------------------------------------------------------------------
2 // <copyright file="_TimerThread.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
9 using System.Collections;
10 using System.Globalization;
11 using System.Threading;
12 using System.Collections.Generic;
13 using System.Runtime.InteropServices;
16 /// <para>Acts as countdown timer, used to measure elapsed time over a [....] operation.</para>
18 internal static class TimerThread {
20 /// <para>Represents a queue of timers, which all have the same duration.</para>
22 internal abstract class Queue {
23 private readonly int m_DurationMilliseconds;
25 internal Queue(int durationMilliseconds) {
26 m_DurationMilliseconds = durationMilliseconds;
30 /// <para>The duration in milliseconds of timers in this queue.</para>
32 internal int Duration {
34 return m_DurationMilliseconds;
39 /// <para>Creates and returns a handle to a new polled timer.</para>
41 internal Timer CreateTimer() {
42 return CreateTimer(null, null);
48 /// <para>Creates and returns a handle to a new timer.</para>
50 internal Timer CreateTimer(Callback callback) {
51 return CreateTimer(callback, null);
56 /// <para>Creates and returns a handle to a new timer with attached context.</para>
58 internal abstract Timer CreateTimer(Callback callback, object context);
62 /// <para>Represents a timer and provides a mechanism to cancel.</para>
64 internal abstract class Timer : IDisposable
66 private readonly int m_StartTimeMilliseconds;
67 private readonly int m_DurationMilliseconds;
69 internal Timer(int durationMilliseconds) {
70 m_DurationMilliseconds = durationMilliseconds;
71 m_StartTimeMilliseconds = Environment.TickCount;
75 /// <para>The duration in milliseconds of timer.</para>
77 internal int Duration {
79 return m_DurationMilliseconds;
84 /// <para>The time (relative to Environment.TickCount) when the timer started.</para>
86 internal int StartTime {
88 return m_StartTimeMilliseconds;
93 /// <para>The time (relative to Environment.TickCount) when the timer will expire.</para>
95 internal int Expiration {
97 return unchecked(m_StartTimeMilliseconds + m_DurationMilliseconds);
102 // Consider removing.
104 /// <para>The amount of time the timer has been running. If it equals Duration, it has fired. 1 less means it has expired but
105 /// not yet fired. Int32.MaxValue is the ceiling - the actual value could be longer. In the case of infinite timers, this
106 /// value becomes unreliable when TickCount wraps (about 46 days).</para>
108 internal int Elapsed {
110 if (HasExpired || Duration == 0) {
114 int now = Environment.TickCount;
115 if (Duration == TimeoutInfinite)
117 return (int) (Math.Min((uint) unchecked(now - StartTime), (uint) Int32.MaxValue);
121 return (IsTickBetween(StartTime, Expiration, now) && Duration > 1) ?
122 (int) (Math.Min((uint) unchecked(now - StartTime), Duration - 2) : Duration - 1;
129 /// <para>The amount of time left on the timer. 0 means it has fired. 1 means it has expired but
130 /// not yet fired. -1 means infinite. Int32.MaxValue is the ceiling - the actual value could be longer.</para>
132 internal int TimeRemaining {
138 if (Duration == Timeout.Infinite) {
139 return Timeout.Infinite;
142 int now = Environment.TickCount;
143 int remaining = IsTickBetween(StartTime, Expiration, now) ?
144 (int) (Math.Min((uint) unchecked(Expiration - now), (uint) Int32.MaxValue)) : 0;
145 return remaining < 2 ? remaining + 1 : remaining;
150 /// <para>Cancels the timer. Returns true if the timer hasn't and won't fire; false if it has or will.</para>
152 internal abstract bool Cancel();
155 /// <para>Whether or not the timer has expired.</para>
157 internal abstract bool HasExpired { get; }
159 public void Dispose()
166 /// <para>Prototype for the callback that is called when a timer expires.</para>
168 internal delegate void Callback(Timer timer, int timeNoticed, object context);
170 private const int c_ThreadIdleTimeoutMilliseconds = 30 * 1000;
171 private const int c_CacheScanPerIterations = 32;
172 private const int c_TickCountResolution = 15;
174 private static LinkedList<WeakReference> s_Queues = new LinkedList<WeakReference>();
175 private static LinkedList<WeakReference> s_NewQueues = new LinkedList<WeakReference>();
176 private static int s_ThreadState = (int) TimerThreadState.Idle; // Really a TimerThreadState, but need an int for Interlocked.
177 private static AutoResetEvent s_ThreadReadyEvent = new AutoResetEvent(false);
178 private static ManualResetEvent s_ThreadShutdownEvent = new ManualResetEvent(false);
179 private static WaitHandle[] s_ThreadEvents;
180 private static int s_CacheScanIteration;
181 private static Hashtable s_QueuesCache = new Hashtable();
183 static TimerThread() {
184 s_ThreadEvents = new WaitHandle[] { s_ThreadShutdownEvent, s_ThreadReadyEvent };
185 AppDomain.CurrentDomain.DomainUnload += new EventHandler(OnDomainUnload);
189 /// <para>The possible states of the timer thread.</para>
191 private enum TimerThreadState {
198 /// <para>The main external entry-point, allows creating new timer queues.</para>
200 internal static Queue CreateQueue(int durationMilliseconds)
202 if (durationMilliseconds == Timeout.Infinite) {
203 return new InfiniteTimerQueue();
206 if (durationMilliseconds < 0) {
207 throw new ArgumentOutOfRangeException("durationMilliseconds");
210 // Queues are held with a weak reference so they can simply be abandoned and automatically cleaned up.
213 queue = new TimerQueue(durationMilliseconds);
214 WeakReference weakQueue = new WeakReference(queue);
215 s_NewQueues.AddLast(weakQueue);
222 /// <para>Alternative cache-based queue factory. Always synchronized.</para>
224 internal static Queue GetOrCreateQueue(int durationMilliseconds) {
225 if (durationMilliseconds == Timeout.Infinite) {
226 return new InfiniteTimerQueue();
229 if (durationMilliseconds < 0) {
230 throw new ArgumentOutOfRangeException("durationMilliseconds");
234 WeakReference weakQueue = (WeakReference) s_QueuesCache[durationMilliseconds];
235 if (weakQueue == null || (queue = (TimerQueue) weakQueue.Target) == null) {
237 weakQueue = (WeakReference) s_QueuesCache[durationMilliseconds];
238 if (weakQueue == null || (queue = (TimerQueue) weakQueue.Target) == null) {
239 queue = new TimerQueue(durationMilliseconds);
240 weakQueue = new WeakReference(queue);
241 s_NewQueues.AddLast(weakQueue);
242 s_QueuesCache[durationMilliseconds] = weakQueue;
244 // Take advantage of this lock to periodically scan the table for garbage.
245 if (++s_CacheScanIteration % c_CacheScanPerIterations == 0) {
246 List<int> garbage = new List<int>();
247 foreach (DictionaryEntry pair in s_QueuesCache) {
248 if (((WeakReference) pair.Value).Target == null) {
249 garbage.Add((int) pair.Key);
252 for (int i = 0; i < garbage.Count; i++) {
253 s_QueuesCache.Remove(garbage[i]);
264 /// <para>Represents a queue of timers of fixed duration.</para>
266 private class TimerQueue : Queue {
267 // This is a GCHandle that holds onto the TimerQueue when active timers are in it.
268 // The TimerThread only holds WeakReferences to it so that it can be collected when the user lets go of it.
269 // But we don't want the user to HAVE to keep a reference to it when timers are active in it.
270 // It gets created when the first timer gets added, and cleaned up when the TimerThread notices it's empty.
271 // The TimerThread will always notice it's empty eventually, since the TimerThread will always wake up and
272 // try to fire the timer, even if it was cancelled and removed prematurely.
273 private IntPtr m_ThisHandle;
275 // This sentinel TimerNode acts as both the head and the tail, allowing nodes to go in and out of the list without updating
276 // any TimerQueue members. m_Timers.Next is the true head, and .Prev the true tail. This also serves as the list's lock.
277 private readonly TimerNode m_Timers;
280 /// <para>Create a new TimerQueue. TimerQueues must be created while s_NewQueues is locked in
281 /// order to synchronize with Shutdown().</para>
283 /// <param name="durationMilliseconds"></param>
284 internal TimerQueue(int durationMilliseconds) :
285 base(durationMilliseconds)
287 // Create the doubly-linked list with a sentinel head and tail so that this member never needs updating.
288 m_Timers = new TimerNode();
289 m_Timers.Next = m_Timers;
290 m_Timers.Prev = m_Timers;
292 // If ReleaseHandle comes back, we need something like this here.
293 // m_HandleFrozen = s_ThreadState == (int) TimerThreadState.Stopped ? 1 : 0;
297 /// <para>Creates new timers. This method is thread-safe.</para>
299 internal override Timer CreateTimer(Callback callback, object context) {
300 TimerNode timer = new TimerNode(callback, context, Duration, m_Timers);
302 // Add this on the tail. (Actually, one before the tail - m_Timers is the sentinel tail.)
303 bool needProd = false;
306 GlobalLog.Assert(m_Timers.Prev.Next == m_Timers, "TimerThread#{0}::CreateTimer()|m_Tail corruption.", Thread.CurrentThread.ManagedThreadId.ToString());
308 // If this is the first timer in the list, we need to create a queue handle and prod the timer thread.
309 if (m_Timers.Next == m_Timers)
311 if (m_ThisHandle == IntPtr.Zero)
313 m_ThisHandle = (IntPtr) GCHandle.Alloc(this);
318 timer.Next = m_Timers;
319 timer.Prev = m_Timers.Prev;
320 m_Timers.Prev.Next = timer;
321 m_Timers.Prev = timer;
324 // If, after we add the new tail, there is a chance that the tail is the next
325 // node to be processed, we need to wake up the timer thread.
335 /// <para>Called by the timer thread to fire the expired timers. Returns true if there are future timers
336 /// in the queue, and if so, also sets nextExpiration.</para>
338 internal bool Fire(out int nextExpiration) {
341 // Check if we got to the end. If so, free the handle.
342 TimerNode timer = m_Timers.Next;
343 if (timer == m_Timers)
347 timer = m_Timers.Next;
348 if (timer == m_Timers)
350 if(m_ThisHandle != IntPtr.Zero)
352 ((GCHandle) m_ThisHandle).Free();
353 m_ThisHandle = IntPtr.Zero;
364 nextExpiration = timer.Expiration;
370 /* Currently unused. If revived, needs to be changed to the new design of m_ThisHandle.
372 /// <para>Release the GCHandle to this object, and prevent it from ever being allocated again.</para>
374 internal void ReleaseHandle()
376 if (Interlocked.Exchange(ref m_HandleFrozen, 1) == 1) {
380 // Add a fake timer to the count. This will prevent the count ever again reaching zero, effectively
381 // disabling the GCHandle alloc/free logic. If it finds that one is allocated, deallocate it.
382 if (Interlocked.Increment(ref m_ActiveTimerCount) != 1) {
384 while ((handle = Interlocked.Exchange(ref m_ThisHandle, IntPtr.Zero)) == IntPtr.Zero)
388 ((GCHandle)handle).Free();
395 /// <para>A special dummy implementation for a queue of timers of infinite duration.</para>
397 private class InfiniteTimerQueue : Queue {
398 internal InfiniteTimerQueue() : base(Timeout.Infinite) { }
401 /// <para>Always returns a dummy infinite timer.</para>
403 internal override Timer CreateTimer(Callback callback, object context)
405 return new InfiniteTimer();
410 /// <para>Internal representation of an individual timer.</para>
412 private class TimerNode : Timer {
413 private TimerState m_TimerState;
414 private Callback m_Callback;
415 private object m_Context;
416 private object m_QueueLock;
417 private TimerNode next;
418 private TimerNode prev;
421 /// <para>Status of the timer.</para>
423 private enum TimerState {
430 internal TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock) : base(durationMilliseconds)
432 if (callback != null)
434 m_Callback = callback;
437 m_TimerState = TimerState.Ready;
438 m_QueueLock = queueLock;
439 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::.ctor()");
442 // A sentinel node - both the head and tail are one, which prevent the head and tail from ever having to be updated.
443 internal TimerNode() : base (0)
445 m_TimerState = TimerState.Sentinel;
449 // Consider removing.
454 return m_TimerState != TimerState.Ready;
459 internal override bool HasExpired {
461 return m_TimerState == TimerState.Fired;
465 internal TimerNode Next
478 internal TimerNode Prev
492 /// <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>
494 internal override bool Cancel() {
495 if (m_TimerState == TimerState.Ready)
499 if (m_TimerState == TimerState.Ready)
501 // Remove it from the list. This keeps the list from getting to big when there are a lot of rapid creations
502 // and cancellations. This is done before setting it to Cancelled to try to prevent the Fire() loop from
503 // seeing it, or if it does, of having to take a lock to synchronize with the state of the list.
507 // Just cleanup. Doesn't need to be in the lock but is easier to have here.
513 m_TimerState = TimerState.Cancelled;
515 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::Cancel() (success)");
521 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::Cancel() (failure)");
526 /// <para>Fires the timer if it is still active and has expired. Returns
527 /// true if it can be deleted, or false if it is still timing.</para>
529 internal bool Fire() {
530 GlobalLog.Assert(m_TimerState != TimerState.Sentinel, "TimerThread#{0}::Fire()|TimerQueue tried to Fire a Sentinel.", Thread.CurrentThread.ManagedThreadId.ToString());
532 if (m_TimerState != TimerState.Ready)
537 // Must get the current tick count within this method so it is guaranteed not to be before
538 // StartTime, which is set in the constructor.
539 int nowMilliseconds = Environment.TickCount;
540 if (IsTickBetween(StartTime, Expiration, nowMilliseconds)) {
541 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() Not firing (" + StartTime + " <= " + nowMilliseconds + " < " + Expiration + ")");
545 bool needCallback = false;
548 if (m_TimerState == TimerState.Ready)
550 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() Firing (" + StartTime + " <= " + nowMilliseconds + " >= " + Expiration + ")");
551 m_TimerState = TimerState.Fired;
553 // Remove it from the list.
557 // Doesn't need to be in the lock but is easier to have here.
560 needCallback = m_Callback != null;
567 Callback callback = m_Callback;
568 object context = m_Context;
571 callback(this, nowMilliseconds, context);
573 catch (Exception exception) {
574 if (NclUtilities.IsFatal(exception)) throw;
576 if (Logging.On) Logging.PrintError(Logging.Web, "TimerThreadTimer#" + StartTime.ToString(NumberFormatInfo.InvariantInfo) + "::Fire() - " + SR.GetString(SR.net_log_exception_in_callback, exception));
577 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() exception in callback: " + exception);
579 // This thread is not allowed to go into user code, so we should never get an exception here.
580 // So, in debug, throw it up, killing the AppDomain. In release, we'll just ignore it.
592 /// <para>A dummy infinite timer.</para>
594 private class InfiniteTimer : Timer {
595 internal InfiniteTimer() : base(Timeout.Infinite) { }
597 private int cancelled;
599 internal override bool HasExpired {
606 /// <para>Cancels the timer. Returns true the first time, false after that.</para>
608 internal override bool Cancel() {
609 return Interlocked.Exchange(ref cancelled, 1) == 0;
614 /// <para>Internal mechanism used when timers are added to wake up / create the thread.</para>
616 private static void Prod() {
617 s_ThreadReadyEvent.Set();
618 TimerThreadState oldState = (TimerThreadState) Interlocked.CompareExchange(
620 (int) TimerThreadState.Running,
621 (int) TimerThreadState.Idle);
623 if (oldState == TimerThreadState.Idle) {
624 new Thread(new ThreadStart(ThreadProc)).Start();
629 /// <para>Thread for the timer. Ignores all exceptions except ThreadAbort. If no activity occurs for a while,
630 /// the thread will shut down.</para>
632 private static void ThreadProc()
635 GlobalLog.SetThreadSource(ThreadKinds.Timer);
636 using (GlobalLog.SetThreadKind(ThreadKinds.System | ThreadKinds.Async)) {
638 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Start");
640 // t_IsTimerThread = true; -- Not used anywhere.
642 // Set this thread as a background thread. On AppDomain/Process shutdown, the thread will just be killed.
643 Thread.CurrentThread.IsBackground = true;
645 // Keep a permanent lock on s_Queues. This lets for example Shutdown() know when this thread isn't running.
648 // If shutdown was recently called, abort here.
649 if (Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Running, (int) TimerThreadState.Running) !=
650 (int) TimerThreadState.Running) {
657 s_ThreadReadyEvent.Reset();
660 // Copy all the new queues to the real queues. Since only this thread modifies the real queues, it doesn't have to lock it.
661 if (s_NewQueues.Count > 0) {
663 for (LinkedListNode<WeakReference> node = s_NewQueues.First; node != null; node = s_NewQueues.First) {
664 s_NewQueues.Remove(node);
665 s_Queues.AddLast(node);
670 int now = Environment.TickCount;
672 bool haveNextTick = false;
673 for (LinkedListNode<WeakReference> node = s_Queues.First; node != null; /* node = node.Next must be done in the body */) {
674 TimerQueue queue = (TimerQueue) node.Value.Target;
676 LinkedListNode<WeakReference> next = node.Next;
677 s_Queues.Remove(node);
682 // Fire() will always return values that should be interpreted as later than 'now' (that is, even if 'now' is
683 // returned, it is 0x100000000 milliseconds in the future). There's also a chance that Fire() will return a value
684 // intended as > 0x100000000 milliseconds from 'now'. Either case will just cause an extra scan through the timers.
685 int nextTickInstance;
686 if (queue.Fire(out nextTickInstance) && (!haveNextTick || IsTickBetween(now, nextTick, nextTickInstance))){
687 nextTick = nextTickInstance;
694 // Figure out how long to wait, taking into account how long the loop took.
695 // Add 15 ms to compensate for poor TickCount resolution (want to guarantee a firing).
696 int newNow = Environment.TickCount;
697 int waitDuration = haveNextTick ?
698 (int) (IsTickBetween(now, nextTick, newNow) ?
699 Math.Min(unchecked((uint) (nextTick - newNow)), (uint) (Int32.MaxValue - c_TickCountResolution)) + c_TickCountResolution :
701 c_ThreadIdleTimeoutMilliseconds;
703 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Waiting for " + waitDuration + "ms");
704 int waitResult = WaitHandle.WaitAny(s_ThreadEvents, waitDuration, false);
706 // 0 is s_ThreadShutdownEvent - die.
707 if (waitResult == 0) {
708 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Awoke, cause: Shutdown");
713 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Awoke, cause: " + (waitResult == WaitHandle.WaitTimeout ? "Timeout" : "Prod"));
715 // If we timed out with nothing to do, shut down.
716 if (waitResult == WaitHandle.WaitTimeout && !haveNextTick) {
717 Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Idle, (int) TimerThreadState.Running);
718 // There could have been one more prod between the wait and the exchange. Check, and abort if necessary.
719 if (s_ThreadReadyEvent.WaitOne(0, false)) {
720 if (Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Running, (int) TimerThreadState.Idle) ==
721 (int) TimerThreadState.Idle) {
731 catch (Exception exception) {
732 if (NclUtilities.IsFatal(exception)) throw;
734 if (Logging.On) Logging.PrintError(Logging.Web, "TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString(NumberFormatInfo.InvariantInfo) + "::ThreadProc() - Exception:" + exception.ToString());
735 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() exception: " + exception);
737 // The only options are to continue processing and likely enter an error-loop,
738 // shut down timers for this AppDomain, or shut down the AppDomain. Go with shutting
739 // down the AppDomain in debug, and going into a loop in retail, but try to make the
740 // loop somewhat slow. Note that in retail, this can only be triggered by OutOfMemory or StackOverflow,
741 // or an thrown within TimerThread - the rest are caught in Fire().
751 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Stop");
759 /// <para>Stops the timer thread and prevents a new one from forming. No more timers can expire.</para>
761 internal static void Shutdown() {
764 // As long as TimerQueues are always created and added to s_NewQueues within the same lock,
765 // this should catch all existing TimerQueues (and all new onew will see s_ThreadState).
767 foreach (WeakReference node in s_NewQueues) {
768 TimerQueue queue = (TimerQueue)node.Target;
770 queue.ReleaseHandle();
775 // Once that thread is gone, release all the remaining GCHandles.
777 foreach (WeakReference node in s_Queues) {
778 TimerQueue queue = (TimerQueue)node.Target;
780 queue.ReleaseHandle();
787 private static void StopTimerThread()
789 Interlocked.Exchange(ref s_ThreadState, (int) TimerThreadState.Stopped);
790 s_ThreadShutdownEvent.Set();
794 /// <para>Helper for deciding whether a given TickCount is before or after a given expiration
795 /// tick count assuming that it can't be before a given starting TickCount.</para>
797 private static bool IsTickBetween(int start, int end, int comparand) {
798 // Assumes that if start and end are equal, they are the same time.
799 // Assumes that if the comparand and start are equal, no time has passed,
800 // and that if the comparand and end are equal, end has occurred.
801 return ((start <= comparand) == (end <= comparand)) != (start <= end);
805 /// <para>When the AppDomain is shut down, the timer thread is stopped.</para>
807 private static void OnDomainUnload(object sender, EventArgs e) {
817 /// <para>This thread static can be used to tell whether the current thread is the TimerThread thread.</para>
820 private static bool t_IsTimerThread;
822 // Consider removing.
823 internal static bool IsTimerThread
827 return t_IsTimerThread;