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 #if MONO_FEATURE_MULTIPLE_APPDOMAINS
186 AppDomain.CurrentDomain.DomainUnload += new EventHandler(OnDomainUnload);
191 /// <para>The possible states of the timer thread.</para>
193 private enum TimerThreadState {
200 /// <para>The main external entry-point, allows creating new timer queues.</para>
202 internal static Queue CreateQueue(int durationMilliseconds)
204 if (durationMilliseconds == Timeout.Infinite) {
205 return new InfiniteTimerQueue();
208 if (durationMilliseconds < 0) {
209 throw new ArgumentOutOfRangeException("durationMilliseconds");
212 // Queues are held with a weak reference so they can simply be abandoned and automatically cleaned up.
215 queue = new TimerQueue(durationMilliseconds);
216 WeakReference weakQueue = new WeakReference(queue);
217 s_NewQueues.AddLast(weakQueue);
224 /// <para>Alternative cache-based queue factory. Always synchronized.</para>
226 internal static Queue GetOrCreateQueue(int durationMilliseconds) {
227 if (durationMilliseconds == Timeout.Infinite) {
228 return new InfiniteTimerQueue();
231 if (durationMilliseconds < 0) {
232 throw new ArgumentOutOfRangeException("durationMilliseconds");
236 WeakReference weakQueue = (WeakReference) s_QueuesCache[durationMilliseconds];
237 if (weakQueue == null || (queue = (TimerQueue) weakQueue.Target) == null) {
239 weakQueue = (WeakReference) s_QueuesCache[durationMilliseconds];
240 if (weakQueue == null || (queue = (TimerQueue) weakQueue.Target) == null) {
241 queue = new TimerQueue(durationMilliseconds);
242 weakQueue = new WeakReference(queue);
243 s_NewQueues.AddLast(weakQueue);
244 s_QueuesCache[durationMilliseconds] = weakQueue;
246 // Take advantage of this lock to periodically scan the table for garbage.
247 if (++s_CacheScanIteration % c_CacheScanPerIterations == 0) {
248 List<int> garbage = new List<int>();
249 foreach (DictionaryEntry pair in s_QueuesCache) {
250 if (((WeakReference) pair.Value).Target == null) {
251 garbage.Add((int) pair.Key);
254 for (int i = 0; i < garbage.Count; i++) {
255 s_QueuesCache.Remove(garbage[i]);
266 /// <para>Represents a queue of timers of fixed duration.</para>
268 private class TimerQueue : Queue {
269 // This is a GCHandle that holds onto the TimerQueue when active timers are in it.
270 // The TimerThread only holds WeakReferences to it so that it can be collected when the user lets go of it.
271 // But we don't want the user to HAVE to keep a reference to it when timers are active in it.
272 // It gets created when the first timer gets added, and cleaned up when the TimerThread notices it's empty.
273 // The TimerThread will always notice it's empty eventually, since the TimerThread will always wake up and
274 // try to fire the timer, even if it was cancelled and removed prematurely.
275 private IntPtr m_ThisHandle;
277 // This sentinel TimerNode acts as both the head and the tail, allowing nodes to go in and out of the list without updating
278 // any TimerQueue members. m_Timers.Next is the true head, and .Prev the true tail. This also serves as the list's lock.
279 private readonly TimerNode m_Timers;
282 /// <para>Create a new TimerQueue. TimerQueues must be created while s_NewQueues is locked in
283 /// order to synchronize with Shutdown().</para>
285 /// <param name="durationMilliseconds"></param>
286 internal TimerQueue(int durationMilliseconds) :
287 base(durationMilliseconds)
289 // Create the doubly-linked list with a sentinel head and tail so that this member never needs updating.
290 m_Timers = new TimerNode();
291 m_Timers.Next = m_Timers;
292 m_Timers.Prev = m_Timers;
294 // If ReleaseHandle comes back, we need something like this here.
295 // m_HandleFrozen = s_ThreadState == (int) TimerThreadState.Stopped ? 1 : 0;
299 /// <para>Creates new timers. This method is thread-safe.</para>
301 internal override Timer CreateTimer(Callback callback, object context) {
302 TimerNode timer = new TimerNode(callback, context, Duration, m_Timers);
304 // Add this on the tail. (Actually, one before the tail - m_Timers is the sentinel tail.)
305 bool needProd = false;
308 GlobalLog.Assert(m_Timers.Prev.Next == m_Timers, "TimerThread#{0}::CreateTimer()|m_Tail corruption.", Thread.CurrentThread.ManagedThreadId.ToString());
310 // If this is the first timer in the list, we need to create a queue handle and prod the timer thread.
311 if (m_Timers.Next == m_Timers)
313 if (m_ThisHandle == IntPtr.Zero)
315 m_ThisHandle = (IntPtr) GCHandle.Alloc(this);
320 timer.Next = m_Timers;
321 timer.Prev = m_Timers.Prev;
322 m_Timers.Prev.Next = timer;
323 m_Timers.Prev = timer;
326 // If, after we add the new tail, there is a chance that the tail is the next
327 // node to be processed, we need to wake up the timer thread.
337 /// <para>Called by the timer thread to fire the expired timers. Returns true if there are future timers
338 /// in the queue, and if so, also sets nextExpiration.</para>
340 internal bool Fire(out int nextExpiration) {
343 // Check if we got to the end. If so, free the handle.
344 TimerNode timer = m_Timers.Next;
345 if (timer == m_Timers)
349 timer = m_Timers.Next;
350 if (timer == m_Timers)
352 if(m_ThisHandle != IntPtr.Zero)
354 ((GCHandle) m_ThisHandle).Free();
355 m_ThisHandle = IntPtr.Zero;
366 nextExpiration = timer.Expiration;
372 /* Currently unused. If revived, needs to be changed to the new design of m_ThisHandle.
374 /// <para>Release the GCHandle to this object, and prevent it from ever being allocated again.</para>
376 internal void ReleaseHandle()
378 if (Interlocked.Exchange(ref m_HandleFrozen, 1) == 1) {
382 // Add a fake timer to the count. This will prevent the count ever again reaching zero, effectively
383 // disabling the GCHandle alloc/free logic. If it finds that one is allocated, deallocate it.
384 if (Interlocked.Increment(ref m_ActiveTimerCount) != 1) {
386 while ((handle = Interlocked.Exchange(ref m_ThisHandle, IntPtr.Zero)) == IntPtr.Zero)
390 ((GCHandle)handle).Free();
397 /// <para>A special dummy implementation for a queue of timers of infinite duration.</para>
399 private class InfiniteTimerQueue : Queue {
400 internal InfiniteTimerQueue() : base(Timeout.Infinite) { }
403 /// <para>Always returns a dummy infinite timer.</para>
405 internal override Timer CreateTimer(Callback callback, object context)
407 return new InfiniteTimer();
412 /// <para>Internal representation of an individual timer.</para>
414 private class TimerNode : Timer {
415 private TimerState m_TimerState;
416 private Callback m_Callback;
417 private object m_Context;
418 private object m_QueueLock;
419 private TimerNode next;
420 private TimerNode prev;
423 /// <para>Status of the timer.</para>
425 private enum TimerState {
432 internal TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock) : base(durationMilliseconds)
434 if (callback != null)
436 m_Callback = callback;
439 m_TimerState = TimerState.Ready;
440 m_QueueLock = queueLock;
441 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::.ctor()");
444 // A sentinel node - both the head and tail are one, which prevent the head and tail from ever having to be updated.
445 internal TimerNode() : base (0)
447 m_TimerState = TimerState.Sentinel;
451 // Consider removing.
456 return m_TimerState != TimerState.Ready;
461 internal override bool HasExpired {
463 return m_TimerState == TimerState.Fired;
467 internal TimerNode Next
480 internal TimerNode Prev
494 /// <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>
496 internal override bool Cancel() {
497 if (m_TimerState == TimerState.Ready)
501 if (m_TimerState == TimerState.Ready)
503 // Remove it from the list. This keeps the list from getting to big when there are a lot of rapid creations
504 // and cancellations. This is done before setting it to Cancelled to try to prevent the Fire() loop from
505 // seeing it, or if it does, of having to take a lock to synchronize with the state of the list.
509 // Just cleanup. Doesn't need to be in the lock but is easier to have here.
515 m_TimerState = TimerState.Cancelled;
517 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::Cancel() (success)");
523 GlobalLog.Print("TimerThreadTimer#" + StartTime.ToString() + "::Cancel() (failure)");
528 /// <para>Fires the timer if it is still active and has expired. Returns
529 /// true if it can be deleted, or false if it is still timing.</para>
531 internal bool Fire() {
532 GlobalLog.Assert(m_TimerState != TimerState.Sentinel, "TimerThread#{0}::Fire()|TimerQueue tried to Fire a Sentinel.", Thread.CurrentThread.ManagedThreadId.ToString());
534 if (m_TimerState != TimerState.Ready)
539 // Must get the current tick count within this method so it is guaranteed not to be before
540 // StartTime, which is set in the constructor.
541 int nowMilliseconds = Environment.TickCount;
542 if (IsTickBetween(StartTime, Expiration, nowMilliseconds)) {
543 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() Not firing (" + StartTime + " <= " + nowMilliseconds + " < " + Expiration + ")");
547 bool needCallback = false;
550 if (m_TimerState == TimerState.Ready)
552 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() Firing (" + StartTime + " <= " + nowMilliseconds + " >= " + Expiration + ")");
553 m_TimerState = TimerState.Fired;
555 // Remove it from the list.
559 // Doesn't need to be in the lock but is easier to have here.
562 needCallback = m_Callback != null;
569 Callback callback = m_Callback;
570 object context = m_Context;
573 callback(this, nowMilliseconds, context);
575 catch (Exception exception) {
576 if (NclUtilities.IsFatal(exception)) throw;
578 if (Logging.On) Logging.PrintError(Logging.Web, "TimerThreadTimer#" + StartTime.ToString(NumberFormatInfo.InvariantInfo) + "::Fire() - " + SR.GetString(SR.net_log_exception_in_callback, exception));
579 GlobalLog.Print("TimerThreadTimer#" + StartTime + "::Fire() exception in callback: " + exception);
581 // This thread is not allowed to go into user code, so we should never get an exception here.
582 // So, in debug, throw it up, killing the AppDomain. In release, we'll just ignore it.
594 /// <para>A dummy infinite timer.</para>
596 private class InfiniteTimer : Timer {
597 internal InfiniteTimer() : base(Timeout.Infinite) { }
599 private int cancelled;
601 internal override bool HasExpired {
608 /// <para>Cancels the timer. Returns true the first time, false after that.</para>
610 internal override bool Cancel() {
611 return Interlocked.Exchange(ref cancelled, 1) == 0;
616 /// <para>Internal mechanism used when timers are added to wake up / create the thread.</para>
618 private static void Prod() {
619 s_ThreadReadyEvent.Set();
620 TimerThreadState oldState = (TimerThreadState) Interlocked.CompareExchange(
622 (int) TimerThreadState.Running,
623 (int) TimerThreadState.Idle);
625 if (oldState == TimerThreadState.Idle) {
626 new Thread(new ThreadStart(ThreadProc)).Start();
631 /// <para>Thread for the timer. Ignores all exceptions except ThreadAbort. If no activity occurs for a while,
632 /// the thread will shut down.</para>
634 private static void ThreadProc()
637 GlobalLog.SetThreadSource(ThreadKinds.Timer);
638 using (GlobalLog.SetThreadKind(ThreadKinds.System | ThreadKinds.Async)) {
640 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Start");
642 // t_IsTimerThread = true; -- Not used anywhere.
644 // Set this thread as a background thread. On AppDomain/Process shutdown, the thread will just be killed.
645 Thread.CurrentThread.IsBackground = true;
647 // Keep a permanent lock on s_Queues. This lets for example Shutdown() know when this thread isn't running.
650 // If shutdown was recently called, abort here.
651 if (Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Running, (int) TimerThreadState.Running) !=
652 (int) TimerThreadState.Running) {
659 s_ThreadReadyEvent.Reset();
662 // Copy all the new queues to the real queues. Since only this thread modifies the real queues, it doesn't have to lock it.
663 if (s_NewQueues.Count > 0) {
665 for (LinkedListNode<WeakReference> node = s_NewQueues.First; node != null; node = s_NewQueues.First) {
666 s_NewQueues.Remove(node);
667 s_Queues.AddLast(node);
672 int now = Environment.TickCount;
674 bool haveNextTick = false;
675 for (LinkedListNode<WeakReference> node = s_Queues.First; node != null; /* node = node.Next must be done in the body */) {
676 TimerQueue queue = (TimerQueue) node.Value.Target;
678 LinkedListNode<WeakReference> next = node.Next;
679 s_Queues.Remove(node);
684 // Fire() will always return values that should be interpreted as later than 'now' (that is, even if 'now' is
685 // returned, it is 0x100000000 milliseconds in the future). There's also a chance that Fire() will return a value
686 // intended as > 0x100000000 milliseconds from 'now'. Either case will just cause an extra scan through the timers.
687 int nextTickInstance;
688 if (queue.Fire(out nextTickInstance) && (!haveNextTick || IsTickBetween(now, nextTick, nextTickInstance))){
689 nextTick = nextTickInstance;
696 // Figure out how long to wait, taking into account how long the loop took.
697 // Add 15 ms to compensate for poor TickCount resolution (want to guarantee a firing).
698 int newNow = Environment.TickCount;
699 int waitDuration = haveNextTick ?
700 (int) (IsTickBetween(now, nextTick, newNow) ?
701 Math.Min(unchecked((uint) (nextTick - newNow)), (uint) (Int32.MaxValue - c_TickCountResolution)) + c_TickCountResolution :
703 c_ThreadIdleTimeoutMilliseconds;
705 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Waiting for " + waitDuration + "ms");
706 int waitResult = WaitHandle.WaitAny(s_ThreadEvents, waitDuration, false);
708 // 0 is s_ThreadShutdownEvent - die.
709 if (waitResult == 0) {
710 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Awoke, cause: Shutdown");
715 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Awoke, cause: " + (waitResult == WaitHandle.WaitTimeout ? "Timeout" : "Prod"));
717 // If we timed out with nothing to do, shut down.
718 if (waitResult == WaitHandle.WaitTimeout && !haveNextTick) {
719 Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Idle, (int) TimerThreadState.Running);
720 // There could have been one more prod between the wait and the exchange. Check, and abort if necessary.
721 if (s_ThreadReadyEvent.WaitOne(0, false)) {
722 if (Interlocked.CompareExchange(ref s_ThreadState, (int) TimerThreadState.Running, (int) TimerThreadState.Idle) ==
723 (int) TimerThreadState.Idle) {
733 catch (Exception exception) {
734 if (NclUtilities.IsFatal(exception)) throw;
736 if (Logging.On) Logging.PrintError(Logging.Web, "TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString(NumberFormatInfo.InvariantInfo) + "::ThreadProc() - Exception:" + exception.ToString());
737 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() exception: " + exception);
739 // The only options are to continue processing and likely enter an error-loop,
740 // shut down timers for this AppDomain, or shut down the AppDomain. Go with shutting
741 // down the AppDomain in debug, and going into a loop in retail, but try to make the
742 // loop somewhat slow. Note that in retail, this can only be triggered by OutOfMemory or StackOverflow,
743 // or an thrown within TimerThread - the rest are caught in Fire().
753 GlobalLog.Print("TimerThread#" + Thread.CurrentThread.ManagedThreadId.ToString() + "::ThreadProc() Stop");
761 /// <para>Stops the timer thread and prevents a new one from forming. No more timers can expire.</para>
763 internal static void Shutdown() {
766 // As long as TimerQueues are always created and added to s_NewQueues within the same lock,
767 // this should catch all existing TimerQueues (and all new onew will see s_ThreadState).
769 foreach (WeakReference node in s_NewQueues) {
770 TimerQueue queue = (TimerQueue)node.Target;
772 queue.ReleaseHandle();
777 // Once that thread is gone, release all the remaining GCHandles.
779 foreach (WeakReference node in s_Queues) {
780 TimerQueue queue = (TimerQueue)node.Target;
782 queue.ReleaseHandle();
789 private static void StopTimerThread()
791 Interlocked.Exchange(ref s_ThreadState, (int) TimerThreadState.Stopped);
792 s_ThreadShutdownEvent.Set();
796 /// <para>Helper for deciding whether a given TickCount is before or after a given expiration
797 /// tick count assuming that it can't be before a given starting TickCount.</para>
799 private static bool IsTickBetween(int start, int end, int comparand) {
800 // Assumes that if start and end are equal, they are the same time.
801 // Assumes that if the comparand and start are equal, no time has passed,
802 // and that if the comparand and end are equal, end has occurred.
803 return ((start <= comparand) == (end <= comparand)) != (start <= end);
807 /// <para>When the AppDomain is shut down, the timer thread is stopped.</para>
809 private static void OnDomainUnload(object sender, EventArgs e) {
819 /// <para>This thread static can be used to tell whether the current thread is the TimerThread thread.</para>
822 private static bool t_IsTimerThread;
824 // Consider removing.
825 internal static bool IsTimerThread
829 return t_IsTimerThread;