From: Stephen Toub Date: Tue, 9 Oct 2018 22:05:46 +0000 (-0400) Subject: Reduce CPU consumption by Timer's FireNextTimers (dotnet/coreclr#20302) X-Git-Tag: submit/tizen/20210909.063632~11030^2~3732 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=dd56fa79bf06c32f05ebb5592fb0ed1f828e8cff;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Reduce CPU consumption by Timer's FireNextTimers (dotnet/coreclr#20302) * Reduce CPU consumption by Timer's FireNextTimers Historically, in certain applications that were Timer-heavy (either via direct use or via wrappers like Task.Delay), timer-related operations like creation (ctor), destruction (Dispose), and firing (internally in FireNextTimers) were relatively expensive. A single linked queue of timers was maintained and protected by a single lock, such that every creation, destruction, and firing would require taking that lock and operating on the list. In .NET Core 2.1, we improved this significantly to reduce contention by splitting the single lock and queue into N partitions, each with its own lock and queue (and native timer that triggers the processing of that queue). This enables lots of threads that would otherwise all be contending for timer creation/destruction/firing to now spread the load across the various partitions. This made a significantly positive and measurable impact on these timer-heavy workloads, in particular for workloads that created and destroyed lots of timers with most never actually firing (e.g. where timers are used to implement timeouts, and most things don't timeout). However, we still see some apps that rely heavily on timers firing, in particular with apps that have tens of thousands or hundreds of thousands of periodic timers, with lots of time spent inside FireNextTimers (the function that's invoked when the native timer for a partition fires to process whatever set of timers may be ready to fire in its queue). This operation currently walks the whole list of that queue's timers, such that it needs to touch and do a little work for every scheduled timer in that partition, even if only one or a few timers actually need to fire. The more timers are scheduled, even if they're for a time far in the future, the more costly FireNextTimers becomes. And as FireNextTimers is invoked while holding that partition's lock, this also then slows down any code paths trying to create/destroy timers on the same partition. This PR attempts to address the most impactful cases of that. Instead of each partition maintaining a single queue, we simply split the queue into two: a "short" list that contains all scheduled timers with a next firing time that's <= some absolute threshold, and a "long" list for the rest. When FireNextTimers is invoked, we walk the "short" list, processing it as we do today. If the current time is less than or equal to the absolute threshold, then we know we don't need to examine the long list and can skip it and the (hopefully) majority of timers it contains. If, however, we're beyond that time or the short list becomes empty, we continue to process the long list as we do today, with the slight modification that we then also move to the short list anything with a due time that puts it at or under the threshold, which is reset to point to a time short into the future. When new timers are added, we just add them to the appropriate list based on their due time. The theory behind this is that the problematic cases are when we have lots of long-lived timers that rarely fire but today we're having to examine them every time any timer fires; by maintaining a short list that ideally has only a small subset of the timers, we can avoid touching the majority of the timers each time FireNextTimers is called, on average. This doesn't change the O(N) complexity of FireNextTimers, but it should often reduce the size of N significantly. Synthetic workloads have shown that it often reduces the cost of FireNextTimers to just 5-10% of what it was previously, and without increasing the cost of the fast add/dispose path measurably. (An alternative approach considered is to use a priority queue / heap for the timer list. This would allow for FireNextTimers to pull off in O(log N) time each of the next timers to fire, hopefully making FireNextTimers much cheaper. However, it comes at the expense of making creation and destruction also O(log N) instead of O(1). And in cases where all timers in the list needed to fire, it would make FireNextTimers O(N log N) instead of O(N). It is, however, an approach that could be pursued if this approach proves less effective in real-world applications than expected.) * Address PR feedback Improve handling of the case where the short list ends up empty. Also fix an issue I noticed around the setting of m_currentAbsoluteThreshold. Commit migrated from https://github.com/dotnet/coreclr/commit/cfe51dd00e069e9fcbee6843e0eee2e80c9b3bd1 --- diff --git a/src/coreclr/src/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/coreclr/src/System.Private.CoreLib/src/System/Threading/Timer.cs index b7e0cfb..ddefa63 100644 --- a/src/coreclr/src/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/coreclr/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 @@ namespace System.Threading #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 @@ namespace System.Threading } } - // // 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 @@ namespace System.Threading 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 @@ namespace System.Threading } } - // // 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,187 @@ namespace System.Threading #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; + + int nowTicks = TickCount; + + // 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). + bool targetShortList = (nowTicks + timer.m_dueTime) - m_currentAbsoluteThreshold <= 0; + 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); + // Switch to process the long list if necessary. + if (listNum == 0) + { + // Determine how much time remains between now and the current threshold. If time remains, + // we can skip processing the long list. We use > rather than >= because, although we + // know that if remaining == 0 no timers in the long list will need to be fired, we + // don't know without looking at them when we'll need to call FireNextTimers again. We + // could in that case just set the next appdomain firing to 1, but we may as well just iterate the + // long list now; otherwise, most timers created in the interim would end up in the long + // list and we'd likely end up paying for another invocation of FireNextTimers that could + // have been delayed longer (to whatever is the current minimum in the long list). + int remaining = m_currentAbsoluteThreshold - nowTicks; + if (remaining > 0) + { + if (m_shortTimers == null && m_longTimers != null) + { + // We don't have any short timers left and we haven't examined the long list, + // which means we likely don't have an accurate nextAppDomainTimerDuration. + // But we do know that nothing in the long list will be firing before or at m_currentAbsoluteThreshold, + // so we can just set nextAppDomainTimerDuration to the difference between then and now. + nextAppDomainTimerDuration = (uint)remaining + 1; + haveTimerToSchedule = true; + } + break; + } + + // Switch to processing the long list. + timer = m_longTimers; + + // Now that we're going to process the long list, update the current threshold. + m_currentAbsoluteThreshold = nowTicks + ShortTimersThresholdMilliseconds; + } + } + + // 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 +389,141 @@ namespace System.Threading 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. + int absoluteDueTime = (int)(nowTicks + dueTime); + bool shouldBeShort = 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. + timer.m_short = shouldBeShort; + LinkTimer(timer); } + else if (timer.m_short != shouldBeShort) + { + // 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); + timer.m_short = shouldBeShort; + 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 +541,8 @@ namespace System.Threading } 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 +556,19 @@ namespace System.Threading 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 +580,10 @@ namespace System.Threading { 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 +596,20 @@ namespace System.Threading 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 +626,9 @@ namespace System.Threading 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 +639,9 @@ namespace System.Threading 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 +662,7 @@ namespace System.Threading 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 +681,6 @@ namespace System.Threading }; } - // // 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 +689,6 @@ namespace System.Threading // 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 +700,6 @@ namespace System.Threading ~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 +708,6 @@ namespace System.Threading // // 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;