diff --git a/src/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/System.Private.CoreLib/src/System/Threading/Timer.cs index b7e0cfb10e3b..be44cb3bf9be 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Timer.cs @@ -2,24 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Security; +using Internal.Runtime.Augments; using Microsoft.Win32; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.ConstrainedExecution; -using System.Runtime.Versioning; +using Microsoft.Win32.SafeHandles; using System.Diagnostics; using System.Diagnostics.Tracing; -using Microsoft.Win32.SafeHandles; - -using Internal.Runtime.Augments; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System.Threading { public delegate void TimerCallback(object state); - // // TimerQueue maintains a list of active timers in this AppDomain. We use a single native timer, supplied by the VM, // to schedule all managed timers in the AppDomain. // @@ -32,14 +26,18 @@ namespace System.Threading // - scheduled background tasks. These typically do fire, but they usually have quite long durations. // So the impact of spending a few extra cycles to fire these is negligible. // - // Because of this, we want to choose a data structure with very fast insert and delete times, but we can live - // with linear traversal times when firing timers. + // Because of this, we want to choose a data structure with very fast insert and delete times, and we can live + // with linear traversal times when firing timers. However, we still want to minimize the number of timers + // we need to traverse while doing the linear walk: in cases where we have lots of long-lived timers as well as + // lots of short-lived timers, when the short-lived timers fire, they incur the cost of walking the long-lived ones. // // The data structure we've chosen is an unordered doubly-linked list of active timers. This gives O(1) insertion - // and removal, and O(N) traversal when finding expired timers. + // and removal, and O(N) traversal when finding expired timers. We maintain two such lists: one for all of the + // timers that'll next fire within a certain threshold, and one for the rest. // // Note that all instance methods of this class require that the caller hold a lock on the TimerQueue instance. - // + // We partition the timers across multiple TimerQueues, each with its own lock and set of short/long lists, + // in order to minimize contention when lots of threads are concurrently creating and destroying timers often. internal class TimerQueue { #region Shared TimerQueue instances @@ -65,20 +63,18 @@ private static TimerQueue[] CreateTimerQueues() #region interface to native per-AppDomain timer - // - // We need to keep our notion of time synchronized with the calls to SleepEx that drive - // the underlying native timer. In Win8, SleepEx does not count the time the machine spends - // sleeping/hibernating. Environment.TickCount (GetTickCount) *does* count that time, - // so we will get out of sync with SleepEx if we use that method. - // - // So, on Win8, we use QueryUnbiasedInterruptTime instead; this does not count time spent - // in sleep/hibernate mode. - // private static int TickCount { get { #if !FEATURE_PAL + // We need to keep our notion of time synchronized with the calls to SleepEx that drive + // the underlying native timer. In Win8, SleepEx does not count the time the machine spends + // sleeping/hibernating. Environment.TickCount (GetTickCount) *does* count that time, + // so we will get out of sync with SleepEx if we use that method. + // + // So, on Win8, we use QueryUnbiasedInterruptTime instead; this does not count time spent + // in sleep/hibernate mode. if (Environment.IsWindows8OrAbove) { ulong time100ns; @@ -98,10 +94,8 @@ private static int TickCount } } - // // We use a SafeHandle to ensure that the native timer is destroyed when the AppDomain is unloaded. - // - private class AppDomainTimerSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + private sealed class AppDomainTimerSafeHandle : SafeHandleZeroOrMinusOneIsInvalid { public AppDomainTimerSafeHandle() : base(true) @@ -123,14 +117,12 @@ protected override bool ReleaseHandle() private bool EnsureAppDomainTimerFiresBy(uint requestedDuration) { - // // The VM's timer implementation does not work well for very long-duration timers. // See kb 950807. // So we'll limit our native timer duration to a "small" value. // This may cause us to attempt to fire timers early, but that's ok - // we'll just see that none of our timers has actually reached its due time, // and schedule the native timer again. - // const uint maxPossibleDuration = 0x0fffffff; uint actualDuration = Math.Min(requestedDuration, maxPossibleDuration); @@ -188,9 +180,7 @@ private bool EnsureAppDomainTimerFiresBy(uint requestedDuration) } } - // // The VM calls this when a native timer fires. - // internal static void AppDomainTimerCallback(int id) { Debug.Assert(id >= 0 && id < Instances.Length && Instances[id].m_id == id); @@ -210,138 +200,192 @@ internal static void AppDomainTimerCallback(int id) #region Firing timers - // - // The list of timers - // - private TimerQueueTimer m_timers; - - - private volatile int m_pauseTicks = 0; // Time when Pause was called - + // The two lists of timers that are part of this TimerQueue. They conform to a single guarantee: + // no timer in m_longTimers has an absolute next firing time <= m_currentAbsoluteThreshold. + // That way, when FireNextTimers is invoked, we always process the short list, and we then only + // process the long list if the current time is greater than m_currentAbsoluteThreshold (or + // if the short list is now empty and we need to process the long list to know when to next + // invoke FireNextTimers). + private TimerQueueTimer m_shortTimers; + private TimerQueueTimer m_longTimers; + + // The current threshold, an absolute time where any timers scheduled to go off at or + // before this time must be queued to the short list. + private int m_currentAbsoluteThreshold = ShortTimersThresholdMilliseconds; + + // Default threshold that separates which timers target m_shortTimers vs m_longTimers. The threshold + // is chosen to balance the number of timers in the small list against the frequency with which + // we need to scan the long list. It's thus somewhat arbitrary and could be changed based on + // observed workload demand. The larger the number, the more timers we'll likely need to enumerate + // every time the appdomain timer fires, but also the more likely it is that when it does we won't + // need to look at the long list because the current time will be <= m_currentAbsoluteThreshold. + private const int ShortTimersThresholdMilliseconds = 333; + + // Time when Pause was called + private volatile int m_pauseTicks = 0; - // // Fire any timers that have expired, and update the native timer to schedule the rest of them. // We're in a thread pool work item here, and if there are multiple timers to be fired, we want // to queue all but the first one. The first may can then be invoked synchronously or queued, // a task left up to our caller, which might be firing timers from multiple queues. - // private void FireNextTimers() { - // - // we fire the first timer on this thread; any other timers that might have fired are queued - // to the ThreadPool. - // + // We fire the first timer on this thread; any other timers that need to be fired + // are queued to the ThreadPool. TimerQueueTimer timerToFireOnThisThread = null; lock (this) { - // prevent ThreadAbort while updating state - try { } - finally + // Since we got here, that means our previous appdomain timer has fired. + m_isAppDomainTimerScheduled = false; + bool haveTimerToSchedule = false; + uint nextAppDomainTimerDuration = uint.MaxValue; + + // Remember the current absolute threshold; we'll use that to evaluate whether + // to process the long list. Then update the current threshold, so that we have a wider + // window with which to move items into the short list and so that we can just + // use due time relative comparisons when deciding whether to move something, as it'll + // all be based off of nowTicks. + int startAbsoluteThreshold = m_currentAbsoluteThreshold; + int nowTicks = TickCount; + m_currentAbsoluteThreshold = nowTicks + ShortTimersThresholdMilliseconds; + + // Sweep through the "short" timers. If the current tick count is greater than + // the current threshold, also sweep through the "long" timers. Finally, as part + // of sweeping the long timers, move anything that'll fire within the next threshold + // to the short list. It's functionally ok if more timers end up in the short list + // than is truly necessary (but not the opposite). + TimerQueueTimer timer = m_shortTimers; + for (int listNum = 0; listNum < 2; listNum++) // short == 0, long == 1 { - // - // since we got here, that means our previous timer has fired. - // - m_isAppDomainTimerScheduled = false; - bool haveTimerToSchedule = false; - uint nextAppDomainTimerDuration = uint.MaxValue; - - int nowTicks = TickCount; - - // - // Sweep through all timers. The ones that have reached their due time - // will fire. We will calculate the next native timer due time from the - // other timers. - // - TimerQueueTimer timer = m_timers; while (timer != null) { - Debug.Assert(timer.m_dueTime != Timeout.UnsignedInfinite); + Debug.Assert(timer.m_dueTime != Timeout.UnsignedInfinite, "A timer in the list must have a valid due time."); + + // Save off the next timer to examine, in case our examination of this timer results + // in our deleting or moving it; we'll continue after with this saved next timer. + TimerQueueTimer next = timer.m_next; uint elapsed = (uint)(nowTicks - timer.m_startTicks); - if (elapsed >= timer.m_dueTime) + int remaining = (int)timer.m_dueTime - (int)elapsed; + if (remaining <= 0) { - // - // Remember the next timer in case we delete this one - // - TimerQueueTimer nextTimer = timer.m_next; + // Timer is ready to fire. if (timer.m_period != Timeout.UnsignedInfinite) { + // This is a repeating timer; schedule it to run again. + + // Discount the extra amount of time that has elapsed since the previous firing time to + // prevent timer ticks from drifting. If enough time has already elapsed for the timer to fire + // again, meaning the timer can't keep up with the short period, have it fire 1 ms from now to + // avoid spinning without a delay. timer.m_startTicks = nowTicks; uint elapsedForNextDueTime = elapsed - timer.m_dueTime; - if (elapsedForNextDueTime < timer.m_period) - { - // Discount the extra amount of time that has elapsed since the previous firing time to - // prevent timer ticks from drifting - timer.m_dueTime = timer.m_period - elapsedForNextDueTime; - } - else - { - // Enough time has elapsed to fire the timer yet again. The timer is not able to keep up - // with the short period, have it fire 1 ms from now to avoid spinning without a delay. - timer.m_dueTime = 1; - } + timer.m_dueTime = (elapsedForNextDueTime < timer.m_period) ? + timer.m_period - elapsedForNextDueTime : + 1; - // - // This is a repeating timer; schedule it to run again. - // + // Update the appdomain timer if this becomes the next timer to fire. if (timer.m_dueTime < nextAppDomainTimerDuration) { haveTimerToSchedule = true; nextAppDomainTimerDuration = timer.m_dueTime; } + + // Validate that the repeating timer is still on the right list. It's likely that + // it started in the long list and was moved to the short list at some point, so + // we now want to move it back to the long list if that's where it belongs. Note that + // if we're currently processing the short list and move it to the long list, we may + // end up revisiting it again if we also enumerate the long list, but we will have already + // updated the due time appropriately so that we won't fire it again (it's also possible + // but rare that we could be moving a timer from the long list to the short list here, + // if the initial due time was set to be long but the timer then had a short period). + // Note that we actually want to be comparing: + // (timer.m_startTicks + timer.m_dueTime) <= m_currentAbsoluteThreshold + // or to account for wrap-around: + // (timer.m_startTicks + timer.m_dueTime) - m_currentAbsoluteThreshold <= 0 + // but we get away with the simpler due-time comparison because we just updated + // m_currentAbsoluteThreshold to be nowTicks+ShortTimersThresholdMilliseconds and + // timer.m_startTicks to be nowTicks, so: + // (timer.m_startTicks + timer.m_dueTime) <= m_currentAbsoluteThreshold + // becomes: + // nowTicks + timer.m_dueTime <= nowTicks + ShortTimersThresholdMilliseconds + // which becomes: + // timer.m_dueTime <= ShortTimersThresholdMilliseconds + // which is valid without concern for wrap-around because we're working with relative + // times that are guaranteed by construction not to wrap. This is the same condition + // used in UpdateTimer. + bool targetShortList = timer.m_dueTime <= ShortTimersThresholdMilliseconds; + if (timer.m_short != targetShortList) + { + MoveTimerToCorrectList(timer, targetShortList); + } } else { - // // Not repeating; remove it from the queue - // DeleteTimer(timer); } - // - // If this is the first timer, we'll fire it on this thread. Otherwise, queue it - // to the ThreadPool. - // + // If this is the first timer, we'll fire it on this thread (after processing + // all others). Otherwise, queue it to the ThreadPool. if (timerToFireOnThisThread == null) + { timerToFireOnThisThread = timer; + } else - QueueTimerCompletion(timer); - - timer = nextTimer; + { + ThreadPool.UnsafeQueueCustomWorkItem(timer, forceGlobal: true); + } } else { - // - // This timer hasn't fired yet. Just update the next time the native timer fires. - // - uint remaining = timer.m_dueTime - elapsed; + // This timer isn't ready to fire. Update the next time the native timer fires if necessary, + // and move this timer to the short list if its remaining time is now at or under the threshold. + if (remaining < nextAppDomainTimerDuration) { haveTimerToSchedule = true; - nextAppDomainTimerDuration = remaining; + nextAppDomainTimerDuration = (uint)remaining; + } + + if (!timer.m_short && remaining <= ShortTimersThresholdMilliseconds) + { + MoveTimerToCorrectList(timer, shortList: true); } - timer = timer.m_next; } + + timer = next; } - if (haveTimerToSchedule) - EnsureAppDomainTimerFiresBy(nextAppDomainTimerDuration); + // If we just finished procesing the short list, we can stop and avoid the long list if + // we're <= the current threshold by which timers were partitioned into the short list + // and the short list is not empty (if it's empty, we need to evaluate the long list to + // know when to next schedule the appdomain timer). Otherwise, we move on to processing + // the long list. + if (listNum == 0) + { + bool processLongList = startAbsoluteThreshold - nowTicks < 0 || m_shortTimers == null; + if (!processLongList) + { + break; + } + + timer = m_longTimers; + } + } + + // If we still have scheduled timers, update the appdomain timer to ensure it fires + // in time for the next one in line. + if (haveTimerToSchedule) + { + EnsureAppDomainTimerFiresBy(nextAppDomainTimerDuration); } } - // // Fire the user timer outside of the lock! - // - if (timerToFireOnThisThread != null) - timerToFireOnThisThread.Fire(); - } - - private static void QueueTimerCompletion(TimerQueueTimer timer) - { - // Can use "unsafe" variant because we take care of capturing and restoring the ExecutionContext. - ThreadPool.UnsafeQueueCustomWorkItem(timer, forceGlobal: true); + timerToFireOnThisThread?.Fire(); } #endregion @@ -350,91 +394,140 @@ private static void QueueTimerCompletion(TimerQueueTimer timer) public bool UpdateTimer(TimerQueueTimer timer, uint dueTime, uint period) { + int nowTicks = TickCount; + + // The timer can be put onto the short list if it's next absolute firing time + // is <= the current absolute threshold. + bool wasShort = timer.m_short; + int absoluteDueTime = (int)(nowTicks + dueTime); + timer.m_short = m_currentAbsoluteThreshold - absoluteDueTime >= 0; + if (timer.m_dueTime == Timeout.UnsignedInfinite) { - // the timer is not in the list; add it (as the head of the list). - timer.m_next = m_timers; - timer.m_prev = null; - if (timer.m_next != null) - timer.m_next.m_prev = timer; - m_timers = timer; + // If the timer wasn't previously scheduled, now add it to the right list. + LinkTimer(timer); + } + else if (timer.m_short != wasShort) + { + // If the timer was previously scheduled, but this update should cause + // it to move over the list threshold in either direction, do so. + UnlinkTimer(timer); + LinkTimer(timer); } + timer.m_dueTime = dueTime; timer.m_period = (period == 0) ? Timeout.UnsignedInfinite : period; - timer.m_startTicks = TickCount; + timer.m_startTicks = nowTicks; return EnsureAppDomainTimerFiresBy(dueTime); } + public void MoveTimerToCorrectList(TimerQueueTimer timer, bool shortList) + { + Debug.Assert(timer.m_dueTime != Timeout.UnsignedInfinite, "Expected timer to be on a list."); + Debug.Assert(timer.m_short != shortList, "Unnecessary if timer is already on the right list."); + + // Unlink it from whatever list it's on, change its list association, then re-link it. + UnlinkTimer(timer); + timer.m_short = shortList; + LinkTimer(timer); + } + + private void LinkTimer(TimerQueueTimer timer) + { + // Use timer.m_short to decide to which list to add. + ref TimerQueueTimer listHead = ref timer.m_short ? ref m_shortTimers : ref m_longTimers; + timer.m_next = listHead; + if (timer.m_next != null) + { + timer.m_next.m_prev = timer; + } + timer.m_prev = null; + listHead = timer; + } + + private void UnlinkTimer(TimerQueueTimer timer) + { + TimerQueueTimer t = timer.m_next; + if (t != null) + { + t.m_prev = timer.m_prev; + } + + if (m_shortTimers == timer) + { + Debug.Assert(timer.m_short); + m_shortTimers = t; + } + else if (m_longTimers == timer) + { + Debug.Assert(!timer.m_short); + m_longTimers = t; + } + + t = timer.m_prev; + if (t != null) + { + t.m_next = timer.m_next; + } + + // At this point the timer is no longer in a list, but its next and prev + // references may still point to other nodes. UnlinkTimer should thus be + // followed by something that overwrites those references, either with null + // if deleting the timer or other nodes if adding it to another list. + } + public void DeleteTimer(TimerQueueTimer timer) { if (timer.m_dueTime != Timeout.UnsignedInfinite) { - if (timer.m_next != null) - timer.m_next.m_prev = timer.m_prev; - if (timer.m_prev != null) - timer.m_prev.m_next = timer.m_next; - if (m_timers == timer) - m_timers = timer.m_next; - + UnlinkTimer(timer); + timer.m_prev = null; + timer.m_next = null; timer.m_dueTime = Timeout.UnsignedInfinite; timer.m_period = Timeout.UnsignedInfinite; timer.m_startTicks = 0; - timer.m_prev = null; - timer.m_next = null; + timer.m_short = false; } } #endregion } - // // A timer in our TimerQueue. - // internal sealed class TimerQueueTimer : IThreadPoolWorkItem { - // // The associated timer queue. - // private readonly TimerQueue m_associatedTimerQueue; - // - // All fields of this class are protected by a lock on TimerQueue.Instance. - // - // The first four fields are maintained by TimerQueue itself. - // + // All mutable fields of this class are protected by a lock on m_associatedTimerQueue. + // The first six fields are maintained by TimerQueue. + + // Links to the next and prev timers in the list. internal TimerQueueTimer m_next; internal TimerQueueTimer m_prev; - // + // true if on the short list; otherwise, false. + internal bool m_short; + // The time, according to TimerQueue.TickCount, when this timer's current interval started. - // internal int m_startTicks; - // // Timeout.UnsignedInfinite if we are not going to fire. Otherwise, the offset from m_startTime when we will fire. - // internal uint m_dueTime; - // // Timeout.UnsignedInfinite if we are a single-shot timer. Otherwise, the repeat interval. - // internal uint m_period; - // // Info about the user's callback - // private readonly TimerCallback m_timerCallback; private readonly object m_state; private readonly ExecutionContext m_executionContext; - - // // When Timer.Dispose(WaitHandle) is used, we need to signal the wait handle only // after all pending callbacks are complete. We set m_canceled to prevent any callbacks that // are already queued from running. We track the number of callbacks currently executing in // m_callbacksRunning. We set m_notifyWhenNoCallbacksRunning only when m_callbacksRunning // reaches zero. - // private int m_callbacksRunning; private volatile bool m_canceled; private volatile WaitHandle m_notifyWhenNoCallbacksRunning; @@ -452,10 +545,8 @@ internal TimerQueueTimer(TimerCallback timerCallback, object state, uint dueTime } m_associatedTimerQueue = TimerQueue.Instances[RuntimeThread.GetCurrentProcessorId() % TimerQueue.Instances.Length]; - // // After the following statement, the timer may fire. No more manipulation of timer state outside of // the lock is permitted beyond this point! - // if (dueTime != Timeout.UnsignedInfinite) Change(dueTime, period); } @@ -469,24 +560,19 @@ internal bool Change(uint dueTime, uint period) if (m_canceled) throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic); - // prevent ThreadAbort while updating state - try { } - finally - { - m_period = period; + m_period = period; - if (dueTime == Timeout.UnsignedInfinite) - { - m_associatedTimerQueue.DeleteTimer(this); - success = true; - } - else - { - if (FrameworkEventSource.IsInitialized && FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer)) - FrameworkEventSource.Log.ThreadTransferSendObj(this, 1, string.Empty, true); + if (dueTime == Timeout.UnsignedInfinite) + { + m_associatedTimerQueue.DeleteTimer(this); + success = true; + } + else + { + if (FrameworkEventSource.IsInitialized && FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer)) + FrameworkEventSource.Log.ThreadTransferSendObj(this, 1, string.Empty, true); - success = m_associatedTimerQueue.UpdateTimer(this, dueTime, period); - } + success = m_associatedTimerQueue.UpdateTimer(this, dueTime, period); } } @@ -498,15 +584,10 @@ public void Close() { lock (m_associatedTimerQueue) { - // prevent ThreadAbort while updating state - try { } - finally + if (!m_canceled) { - if (!m_canceled) - { - m_canceled = true; - m_associatedTimerQueue.DeleteTimer(this); - } + m_canceled = true; + m_associatedTimerQueue.DeleteTimer(this); } } } @@ -519,25 +600,20 @@ public bool Close(WaitHandle toSignal) lock (m_associatedTimerQueue) { - // prevent ThreadAbort while updating state - try { } - finally + if (m_canceled) { - if (m_canceled) - { - success = false; - } - else - { - m_canceled = true; - m_notifyWhenNoCallbacksRunning = toSignal; - m_associatedTimerQueue.DeleteTimer(this); + success = false; + } + else + { + m_canceled = true; + m_notifyWhenNoCallbacksRunning = toSignal; + m_associatedTimerQueue.DeleteTimer(this); - if (m_callbacksRunning == 0) - shouldSignal = true; + if (m_callbacksRunning == 0) + shouldSignal = true; - success = true; - } + success = true; } } @@ -554,14 +630,9 @@ internal void Fire() lock (m_associatedTimerQueue) { - // prevent ThreadAbort while updating state - try { } - finally - { - canceled = m_canceled; - if (!canceled) - m_callbacksRunning++; - } + canceled = m_canceled; + if (!canceled) + m_callbacksRunning++; } if (canceled) @@ -572,14 +643,9 @@ internal void Fire() bool shouldSignal = false; lock (m_associatedTimerQueue) { - // prevent ThreadAbort while updating state - try { } - finally - { - m_callbacksRunning--; - if (m_canceled && m_callbacksRunning == 0 && m_notifyWhenNoCallbacksRunning != null) - shouldSignal = true; - } + m_callbacksRunning--; + if (m_canceled && m_callbacksRunning == 0 && m_notifyWhenNoCallbacksRunning != null) + shouldSignal = true; } if (shouldSignal) @@ -600,7 +666,7 @@ internal void CallCallback() if (FrameworkEventSource.IsInitialized && FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer)) FrameworkEventSource.Log.ThreadTransferReceiveObj(this, 1, string.Empty); - // call directly if EC flow is suppressed + // Call directly if EC flow is suppressed ExecutionContext context = m_executionContext; if (context == null) { @@ -619,7 +685,6 @@ internal void CallCallback() }; } - // // TimerHolder serves as an intermediary between Timer and TimerQueueTimer, releasing the TimerQueueTimer // if the Timer is collected. // This is necessary because Timer itself cannot use its finalizer for this purpose. If it did, @@ -628,7 +693,6 @@ internal void CallCallback() // via first-class APIs), but Timer has never offered this, and adding it now would be a breaking // change, because any code that happened to be suppressing finalization of Timer objects would now // unwittingly be changing the lifetime of those timers. - // internal sealed class TimerHolder { internal TimerQueueTimer m_timer; @@ -640,7 +704,6 @@ public TimerHolder(TimerQueueTimer timer) ~TimerHolder() { - // // If shutdown has started, another thread may be suspended while holding the timer lock. // So we can't safely close the timer. // @@ -649,7 +712,6 @@ public TimerHolder(TimerQueueTimer timer) // // Note that in either case, the Timer still won't fire, because ThreadPool threads won't be // allowed to run in this AppDomain. - // if (Environment.HasShutdownStarted) return;